tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 252
 253        See also: `SearchByTicker()`, `SearchInstruments()`.
 254        """
 255
 256        self.figi = ""
 257        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 258
 259        See also: `SearchByFIGI()`, `SearchInstruments()`.
 260        """
 261
 262        self.depth = 1
 263        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 264
 265        See also: `GetCurrentPrices()`.
 266        """
 267
 268        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 269        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 270
 271        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 272        """
 273
 274        uLogger.debug("Broker API server: {}".format(self.server))
 275
 276        self.timeout = 15
 277        """Server operations timeout in seconds. Default: `15`.
 278
 279        See also: `SendAPIRequest()`.
 280        """
 281
 282        self.headers = {
 283            "Content-Type": "application/json",
 284            "accept": "application/json",
 285            "Authorization": "Bearer {}".format(self.token),
 286            "x-app-name": "Tim55667757.TKSBrokerAPI",
 287        }
 288        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.body = None
 294        """Request body which send to broker server. Default: `None`.
 295
 296        See also: `SendAPIRequest()`.
 297        """
 298
 299        self.moreDebug = False
 300        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 301
 302        self.historyFile = None
 303        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 304
 305        See also: `History()`.
 306        """
 307
 308        self.htmlHistoryFile = "index.html"
 309        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 310
 311        See also: `ShowHistoryChart()`.
 312        """
 313
 314        self.instrumentsFile = "instruments.md"
 315        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 316
 317        See also: `ShowInstrumentsInfo()`.
 318        """
 319
 320        self.searchResultsFile = "search-results.md"
 321        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 322
 323        See also: `SearchInstruments()`.
 324        """
 325
 326        self.pricesFile = "prices.md"
 327        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 328
 329        See also: `GetListOfPrices()`.
 330        """
 331
 332        self.infoFile = "info.md"
 333        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 334
 335        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 336        """
 337
 338        self.bondsXLSXFile = "ext-bonds.xlsx"
 339        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 340        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 341
 342        See also: `ExtendBondsData()`.
 343        """
 344
 345        self.calendarFile = "calendar.md"
 346        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 347        
 348        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 349
 350        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 351        """
 352
 353        self.overviewFile = "overview.md"
 354        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 355
 356        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 357        """
 358
 359        self.overviewDigestFile = "overview-digest.md"
 360        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 361
 362        See also: `Overview()` with parameter `details="digest"`.
 363        """
 364
 365        self.overviewPositionsFile = "overview-positions.md"
 366        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 367
 368        See also: `Overview()` with parameter `details="positions"`.
 369        """
 370
 371        self.overviewOrdersFile = "overview-orders.md"
 372        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 373
 374        See also: `Overview()` with parameter `details="orders"`.
 375        """
 376
 377        self.overviewAnalyticsFile = "overview-analytics.md"
 378        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 379
 380        See also: `Overview()` with parameter `details="analytics"`.
 381        """
 382
 383        self.overviewBondsCalendarFile = "overview-calendar.md"
 384        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 385
 386        See also: `Overview()` with parameter `details="calendar"`.
 387        """
 388
 389        self.reportFile = "deals.md"
 390        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 391
 392        See also: `Deals()`.
 393        """
 394
 395        self.withdrawalLimitsFile = "limits.md"
 396        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 397
 398        See also: `OverviewLimits()` and `RequestLimits()`.
 399        """
 400
 401        self.userInfoFile = "user-info.md"
 402        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 403
 404        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 405        """
 406
 407        self.userAccountsFile = "accounts.md"
 408        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 409
 410        See also: `OverviewAccounts()`, `RequestAccounts()`.
 411        """
 412
 413        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 414        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 415
 416        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 417
 418        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 419        """
 420
 421        self.iList = None  # init iList for raw instruments data
 422        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 423        
 424        See also: `Listing()`, `DumpInstruments()`.
 425        """
 426
 427        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 428        if useCache:
 429            if os.path.exists(self.iListDumpFile):
 430                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 431                curTime = datetime.now(tzutc())
 432
 433                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 434                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 435
 436                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 437
 438                else:
 439                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 440
 441                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 442                        os.path.abspath(self.iListDumpFile),
 443                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 444                    ))
 445
 446            else:
 447                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 448                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 449
 450        else:
 451            self.iList = self.Listing()  # request new raw instruments data from broker server
 452            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 453
 454        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 455        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 456
 457        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 458        """
 459
 460    def _ParseJSON(self, rawData="{}") -> dict:
 461        """
 462        Parse JSON from response string.
 463
 464        :param rawData: this is a string with JSON-formatted text.
 465        :return: JSON (dictionary), parsed from server response string.
 466        """
 467        responseJSON = json.loads(rawData) if rawData else {}
 468
 469        if self.moreDebug:
 470            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 471
 472        return responseJSON
 473
 474    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 475        """
 476        Send GET or POST request to broker server and receive JSON object.
 477
 478        self.header: must be defining with dictionary of headers.
 479        self.body: if define then used as request body. None by default.
 480        self.timeout: global request timeout, 15 seconds by default.
 481        :param url: url with REST request.
 482        :param reqType: send "GET" or "POST" request. "GET" by default.
 483        :param retry: how many times retry after first request if an 5xx server errors occurred.
 484        :param pause: sleep time in seconds between retries.
 485        :return: response JSON (dictionary) from broker.
 486        """
 487        if reqType not in ("GET", "POST"):
 488            uLogger.error("You can define request type: 'GET' or 'POST'!")
 489            raise Exception("Incorrect value")
 490
 491        if self.moreDebug:
 492            uLogger.debug("Request parameters:")
 493            uLogger.debug("    - REST API URL: {}".format(url))
 494            uLogger.debug("    - request type: {}".format(reqType))
 495            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 496            uLogger.debug("    - body:\n{}".format(self.body))
 497
 498        # fast hack to avoid all operations with some tickers/FIGI
 499        responseJSON = {}
 500        oK = True
 501        for item in self.exclude:
 502            if item in url:
 503                if self.moreDebug:
 504                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 505
 506                oK = False
 507                break
 508
 509        if oK:
 510            counter = 0
 511            response = None
 512            errMsg = ""
 513
 514            while not response and counter <= retry:
 515                if reqType == "GET":
 516                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 517
 518                if reqType == "POST":
 519                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 520
 521                if self.moreDebug:
 522                    uLogger.debug("Response:")
 523                    uLogger.debug("    - status code: {}".format(response.status_code))
 524                    uLogger.debug("    - reason: {}".format(response.reason))
 525                    uLogger.debug("    - body length: {}".format(len(response.text)))
 526                    uLogger.debug("    - headers:\n{}".format(response.headers))
 527
 528                # Server returns some headers:
 529                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 530                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 531                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 532                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 533                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 534                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 535                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 536                    sleep(rateLimitWait)
 537
 538                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 539                if 400 <= response.status_code < 500:
 540                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 541                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 542                    counter = retry + 1
 543
 544                if 500 <= response.status_code < 600:
 545                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 546                    uLogger.debug("    - not oK, {}".format(errMsg))
 547                    counter += 1
 548
 549                    if counter <= retry:
 550                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 551                        sleep(pause)
 552
 553            responseJSON = self._ParseJSON(rawData=response.text)
 554
 555            if errMsg:
 556                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 557                uLogger.error("    - not oK, {}".format(errMsg))
 558
 559        return responseJSON
 560
 561    def _IUpdater(self, iType: str) -> tuple:
 562        """
 563        Request instrument by type from server. See available API methods for instruments:
 564        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 565        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 566        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 567        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 568        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 569
 570        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 571        :return: tuple with iType name and list of available instruments of current type for defined user token.
 572        """
 573        result = []
 574
 575        if iType in TKS_INSTRUMENTS:
 576            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 577
 578            # all instruments have the same body in API v2 requests:
 579            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 580            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 581            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 582
 583        return iType, result
 584
 585    def _IWrapper(self, kwargs):
 586        """
 587        Wrapper runs instrument's update method `_IUpdater()`.
 588        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 589        """
 590        return self._IUpdater(**kwargs)
 591
 592    def Listing(self) -> dict:
 593        """
 594        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 595
 596        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 597        """
 598        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 599        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 600
 601        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 602        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 603        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 604
 605        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 606        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 607        poolUpdater.close()
 608
 609        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 610        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 611        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 612
 613        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 614        for iType in iList.keys():
 615            for ticker in iList[iType]:
 616                iList[iType][ticker]["type"] = iType
 617
 618                if "minPriceIncrement" in iList[iType][ticker].keys():
 619                    iList[iType][ticker]["step"] = NanoToFloat(
 620                        iList[iType][ticker]["minPriceIncrement"]["units"],
 621                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 622                    )
 623
 624                else:
 625                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 626
 627        return iList
 628
 629    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 630        """
 631        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 632
 633        See also: `DumpInstruments()`, `Listing()`.
 634
 635        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 636                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 637        """
 638        if self.iListDumpFile is None or not self.iListDumpFile:
 639            uLogger.error("Output name of dump file must be defined!")
 640            raise Exception("Filename required")
 641
 642        if not self.iList or forceUpdate:
 643            self.iList = self.Listing()
 644
 645        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 646
 647        # Save as XLSX with separated sheets for every type of instruments:
 648        with pd.ExcelWriter(
 649                path=xlsxDumpFile,
 650                date_format=TKS_DATE_FORMAT,
 651                datetime_format=TKS_DATE_TIME_FORMAT,
 652                mode="w",
 653        ) as writer:
 654            for iType in TKS_INSTRUMENTS:
 655                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 656                df = df[sorted(df)]  # sorted by column names
 657                df = df.applymap(
 658                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 659                    na_action="ignore",
 660                )  # converting numbers from nano-type to float in every cell
 661                df.to_excel(
 662                    writer,
 663                    sheet_name=iType,
 664                    encoding="UTF-8",
 665                    freeze_panes=(1, 1),
 666                )  # saving as XLSX-file with freeze first row and column as headers
 667
 668        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 669
 670    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 671        """
 672        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 673        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 674
 675        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 676
 677        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 678                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 679        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 680        """
 681        if self.iListDumpFile is None or not self.iListDumpFile:
 682            uLogger.error("Output name of dump file must be defined!")
 683            raise Exception("Filename required")
 684
 685        if not self.iList or forceUpdate:
 686            self.iList = self.Listing()
 687
 688        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 689        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 690            fH.write(jsonDump)
 691
 692        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 693
 694        return jsonDump
 695
 696    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 697        """
 698        Show information about one instrument defined by json data and prints it in Markdown format.
 699
 700        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 701
 702        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 703        :param show: if `True` then also printing information about instrument and its current price.
 704        :return: multilines text in Markdown format with information about one instrument.
 705        """
 706        splitLine = "|                                                             |                                                        |\n"
 707        infoText = ""
 708
 709        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 710            info = [
 711                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 712                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 713                "| Parameters                                                  | Values                                                 |\n",
 714                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 715                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 716                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 717            ]
 718
 719            if "sector" in iJSON.keys() and iJSON["sector"]:
 720                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 721
 722            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 723                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 724                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 725            )))
 726
 727            info.extend([
 728                splitLine,
 729                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 730                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 731            ])
 732
 733            if "isin" in iJSON.keys() and iJSON["isin"]:
 734                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 735
 736            if "classCode" in iJSON.keys():
 737                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 738
 739            info.extend([
 740                splitLine,
 741                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 742                splitLine,
 743                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 744                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 745                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 746            ])
 747
 748            if iJSON["figi"]:
 749                self.figi = iJSON["figi"]
 750                iJSON = iJSON | self.RequestTradingStatus()
 751
 752                info.extend([
 753                    splitLine,
 754                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 755                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 756                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 757                ])
 758
 759            info.append(splitLine)
 760
 761            if "type" in iJSON.keys() and iJSON["type"]:
 762                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 763
 764            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 765                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 766
 767            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 768                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 769
 770            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 771                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 772
 773            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 774                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 775
 776            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 777                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 778
 779            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 780                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 781
 782            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 783                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 784
 785            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 786                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 787
 788            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 789                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 790
 791            if "currency" in iJSON.keys():
 792                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 793
 794            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 795                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 796
 797            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 798                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 801                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 804                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 807                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 810                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 811
 812            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 813                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 814
 815            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 816                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 817
 818            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 819                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 820
 821            iExt = None
 822            if iJSON["type"] == "Bonds":
 823                info.extend([
 824                    splitLine,
 825                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 826                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 827                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 828                        iJSON["nominal"]["currency"],
 829                    )),
 830                ])
 831
 832                if "floatingCouponFlag" in iJSON.keys():
 833                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 834
 835                if "amortizationFlag" in iJSON.keys():
 836                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 837
 838                info.append(splitLine)
 839
 840                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 841                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 842
 843                if iJSON["figi"]:
 844                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 845
 846                    info.extend([
 847                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 848                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 849                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 850                    ])
 851
 852                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 853                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 854                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 855                        iJSON["aciValue"]["currency"]
 856                    )))
 857
 858            if "currentPrice" in iJSON.keys():
 859                info.append(splitLine)
 860
 861                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 862                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 863
 864                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 865                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 866                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 867                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 868                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 869
 870                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 871                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 872
 873                info.extend([
 874                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 875                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 876                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 877                    )),
 878                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 879                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 880                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 881                    )),
 882                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 883                        "{:.2f}%{}".format(
 884                            iJSON["currentPrice"]["changes"],
 885                            " ({}{:.2f} {})".format(
 886                                "+" if bondChangesDelta > 0 else "",
 887                                bondChangesDelta,
 888                                aciCurrency
 889                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 890                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 891                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 892                                currency
 893                            ),
 894                        )
 895                    ),
 896                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 897                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 898                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 899                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 902                    )),
 903                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 904                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 905                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 906                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 907                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 908                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 909                    )),
 910                ])
 911
 912            if "lot" in iJSON.keys():
 913                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 914
 915            if "step" in iJSON.keys() and iJSON["step"] != 0:
 916                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 917
 918            # Add bond payment calendar:
 919            if iJSON["type"] == "Bonds":
 920                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 921                info.extend(["\n", strCalendar])
 922
 923            infoText += "".join(info)
 924
 925            if show:
 926                uLogger.info("{}".format(infoText))
 927
 928            else:
 929                uLogger.debug("{}".format(infoText))
 930
 931            if self.infoFile is not None:
 932                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 933                    fH.write(infoText)
 934
 935                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 936
 937        return infoText
 938
 939    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 940        """
 941        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 942
 943        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 944        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 945        :return: JSON formatted data with information about instrument.
 946        """
 947        tickerJSON = {}
 948        if self.moreDebug:
 949            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 950
 951        if not self.ticker:
 952            uLogger.warning("self.ticker variable is not be empty!")
 953
 954        else:
 955            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 956                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 957                raise Exception("Instrument not allowed")
 958
 959            if not self.iList:
 960                self.iList = self.Listing()
 961
 962            if self.ticker in self.iList["Shares"].keys():
 963                tickerJSON = self.iList["Shares"][self.ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Currencies"].keys():
 968                tickerJSON = self.iList["Currencies"][self.ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Bonds"].keys():
 973                tickerJSON = self.iList["Bonds"][self.ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Etfs"].keys():
 978                tickerJSON = self.iList["Etfs"][self.ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 981
 982            elif self.ticker in self.iList["Futures"].keys():
 983                tickerJSON = self.iList["Futures"][self.ticker]
 984                if self.moreDebug:
 985                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 986
 987        if tickerJSON:
 988            self.figi = tickerJSON["figi"]
 989
 990            if requestPrice:
 991                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 992
 993                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 994                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 995
 996                else:
 997                    tickerJSON["currentPrice"]["changes"] = 0
 998
 999            if show:
1000                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1001
1002        else:
1003            if show:
1004                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1005
1006        return tickerJSON
1007
1008    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1009        """
1010        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1011
1012        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1013        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1014        :return: JSON formatted data with information about instrument.
1015        """
1016        figiJSON = {}
1017        if self.moreDebug:
1018            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1019
1020        if not self.figi:
1021            uLogger.warning("self.figi variable is not be empty!")
1022
1023        else:
1024            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1025                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1026                raise Exception("Instrument not allowed")
1027
1028            if not self.iList:
1029                self.iList = self.Listing()
1030
1031            for item in self.iList["Shares"].keys():
1032                if self.figi == self.iList["Shares"][item]["figi"]:
1033                    figiJSON = self.iList["Shares"][item]
1034
1035                    if self.moreDebug:
1036                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1037
1038                    break
1039
1040            if not figiJSON:
1041                for item in self.iList["Currencies"].keys():
1042                    if self.figi == self.iList["Currencies"][item]["figi"]:
1043                        figiJSON = self.iList["Currencies"][item]
1044
1045                        if self.moreDebug:
1046                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Bonds"].keys():
1052                    if self.figi == self.iList["Bonds"][item]["figi"]:
1053                        figiJSON = self.iList["Bonds"][item]
1054
1055                        if self.moreDebug:
1056                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Etfs"].keys():
1062                    if self.figi == self.iList["Etfs"][item]["figi"]:
1063                        figiJSON = self.iList["Etfs"][item]
1064
1065                        if self.moreDebug:
1066                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1067
1068                        break
1069
1070            if not figiJSON:
1071                for item in self.iList["Futures"].keys():
1072                    if self.figi == self.iList["Futures"][item]["figi"]:
1073                        figiJSON = self.iList["Futures"][item]
1074
1075                        if self.moreDebug:
1076                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1077
1078                        break
1079
1080        if figiJSON:
1081            self.figi = figiJSON["figi"]
1082            self.ticker = figiJSON["ticker"]
1083
1084            if requestPrice:
1085                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1086
1087                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1088                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1089
1090                else:
1091                    figiJSON["currentPrice"]["changes"] = 0
1092
1093            if show:
1094                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1095
1096        else:
1097            if show:
1098                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1099
1100        return figiJSON
1101
1102    def GetCurrentPrices(self, show: bool = True) -> dict:
1103        """
1104        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1105        `{"buy": [{"price": 1243.8, "quantity": 193},
1106                  {"price": 1244.0, "quantity": 168},
1107                  {"price": 1244.8, "quantity": 5},
1108                  {"price": 1245.0, "quantity": 61},
1109                  {"price": 1245.4, "quantity": 60}],
1110          "sell": [{"price": 1243.6, "quantity": 8},
1111                   {"price": 1242.6, "quantity": 10},
1112                   {"price": 1242.4, "quantity": 18},
1113                   {"price": 1242.2, "quantity": 50},
1114                   {"price": 1242.0, "quantity": 113}],
1115          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1116        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1117        - sell: list of dicts with Buyers prices,
1118            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1119            - quantity: volume value by current price in lots,
1120        - limitUp: current trade session limit price, maximum,
1121        - limitDown: current trade session limit price, minimum,
1122        - lastPrice: last deal price of the instrument,
1123        - closePrice: previous trade session close price of the instrument.
1124
1125        See also: `SearchByTicker()` and `SearchByFIGI()`.
1126        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1127        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1128
1129        :param show: if `True` then print DOM to log and console.
1130        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1131                 If an error occurred then returns an empty record:
1132                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1133        """
1134        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1135
1136        if self.depth < 1:
1137            uLogger.error("Depth of Market (DOM) must be >=1!")
1138            raise Exception("Incorrect value")
1139
1140        if not (self.ticker or self.figi):
1141            uLogger.error("self.ticker or self.figi variables must be defined!")
1142            raise Exception("Ticker or FIGI required")
1143
1144        if self.ticker and not self.figi:
1145            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1146            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1147
1148        if not self.ticker and self.figi:
1149            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1150            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1151
1152        if not self.figi:
1153            uLogger.error("FIGI is not defined!")
1154            raise Exception("Ticker or FIGI required")
1155
1156        else:
1157            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1158
1159            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1160            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1161            self.body = str({"figi": self.figi, "depth": self.depth})
1162            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1163
1164            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1165                # list of dicts with sellers orders:
1166                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1167
1168                # list of dicts with buyers orders:
1169                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1170
1171                # max price of instrument at this time:
1172                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1173
1174                # min price of instrument at this time:
1175                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1176
1177                # last price of deal with instrument:
1178                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1179
1180                # last close price of instrument:
1181                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1182
1183            else:
1184                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1185                uLogger.debug("Server response: {}".format(pricesResponse))
1186
1187            if show:
1188                if prices["buy"] or prices["sell"]:
1189                    info = [
1190                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1191                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1192                            self.ticker,
1193                            self.figi,
1194                            self.depth,
1195                        ),
1196                        "-" * 60, "\n",
1197                        "             Orders of Buyers | Orders of Sellers\n",
1198                        "-" * 60, "\n",
1199                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1200                        "-" * 60, "\n",
1201                    ]
1202
1203                    if not prices["buy"]:
1204                        info.append("                              | No orders!\n")
1205                        sumBuy = 0
1206
1207                    else:
1208                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1209                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1210                        for item in maxMinSorted:
1211                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1212
1213                    if not prices["sell"]:
1214                        info.append("No orders!                    |\n")
1215                        sumSell = 0
1216
1217                    else:
1218                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1219                        for item in prices["sell"]:
1220                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1221
1222                    info.extend([
1223                        "-" * 60, "\n",
1224                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1225                        "-" * 60, "\n",
1226                    ])
1227
1228                    infoText = "".join(info)
1229
1230                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1231
1232                else:
1233                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1234
1235        return prices
1236
1237    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1238        """
1239        This method get and show information about all available broker instruments for current user account.
1240        If `instrumentsFile` string is not empty then also save information to this file.
1241
1242        :param show: if `True` then print results to console, if `False` — print only to file.
1243        :return: multi-lines string with all available broker instruments
1244        """
1245        if not self.iList:
1246            self.iList = self.Listing()
1247
1248        info = [
1249            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1250            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1251        ]
1252
1253        # add instruments count by type:
1254        for iType in self.iList.keys():
1255            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1256
1257        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1258        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1259
1260        # generating info tables with all instruments by type:
1261        for iType in self.iList.keys():
1262            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1263
1264            for instrument in self.iList[iType].keys():
1265                iName = self.iList[iType][instrument]["name"]  # instrument's name
1266                if len(iName) > 57:
1267                    iName = "{}...".format(iName[:54])  # right trim for a long string
1268
1269                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1270                    self.iList[iType][instrument]["ticker"],
1271                    iName,
1272                    self.iList[iType][instrument]["figi"],
1273                    self.iList[iType][instrument]["currency"],
1274                    self.iList[iType][instrument]["lot"],
1275                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1276                ))
1277
1278        infoText = "".join(info)
1279
1280        if show:
1281            uLogger.info(infoText)
1282
1283        if self.instrumentsFile:
1284            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1285                fH.write(infoText)
1286
1287            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1288
1289        return infoText
1290
1291    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1292        """
1293        This method search and show information about instruments by part of its ticker, FIGI or name.
1294        If `searchResultsFile` string is not empty then also save information to this file.
1295
1296        :param pattern: string with part of ticker, FIGI or instrument's name.
1297        :param show: if `True` then print results to console, if `False` — return list of result only.
1298        :return: list of dictionaries with all found instruments.
1299        """
1300        if not self.iList:
1301            self.iList = self.Listing()
1302
1303        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1304        compiledPattern = re.compile(pattern, re.IGNORECASE)
1305
1306        for iType in self.iList:
1307            for instrument in self.iList[iType].values():
1308                searchResult = compiledPattern.search(" ".join(
1309                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1310                ))
1311
1312                if searchResult:
1313                    searchResults[iType][instrument["ticker"]] = instrument
1314
1315        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1316        info = [
1317            "# Search results\n\n",
1318            "* **Search pattern:** [{}]\n".format(pattern),
1319            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1320            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1321        ]
1322        infoShort = info[:]
1323
1324        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1325        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1326        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1327
1328        if resultsLen == 0:
1329            info.append("\nNo results\n")
1330            infoShort.append("\nNo results\n")
1331            uLogger.warning("No results. Try changing your search pattern.")
1332
1333        else:
1334            for iType in searchResults:
1335                iTypeValuesCount = len(searchResults[iType].values())
1336                if iTypeValuesCount > 0:
1337                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1338                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339
1340                    for instrument in searchResults[iType].values():
1341                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1342                            instrument["type"],
1343                            instrument["ticker"],
1344                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1345                            instrument["figi"],
1346                        ))
1347
1348                    if iTypeValuesCount <= 5:
1349                        infoShort.extend(info[-iTypeValuesCount:])
1350
1351                    else:
1352                        infoShort.extend(info[-5:])
1353                        infoShort.append(skippedLine)
1354
1355        infoText = "".join(info)
1356        infoTextShort = "".join(infoShort)
1357
1358        if show:
1359            uLogger.info(infoTextShort)
1360            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1361
1362        if self.searchResultsFile:
1363            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1364                fH.write(infoText)
1365
1366            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1367
1368        return searchResults
1369
1370    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1371        """
1372        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1373
1374        :param instruments: list of strings with tickers or FIGIs.
1375        :return: list with unique instrument FIGIs only.
1376        """
1377        requestedInstruments = []
1378        for iName in instruments:
1379            if iName not in self.aliases.keys():
1380                if iName not in requestedInstruments:
1381                    requestedInstruments.append(iName)
1382
1383            else:
1384                if iName not in requestedInstruments:
1385                    if self.aliases[iName] not in requestedInstruments:
1386                        requestedInstruments.append(self.aliases[iName])
1387
1388        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1389
1390        onlyUniqueFIGIs = []
1391        for iName in requestedInstruments:
1392            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1393                continue
1394
1395            self.ticker = iName
1396            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1397
1398            if not iData:
1399                self.ticker = ""
1400                self.figi = iName
1401
1402                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1403
1404                if not iData:
1405                    self.figi = ""
1406                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1407
1408            if iData and iData["figi"] not in onlyUniqueFIGIs:
1409                onlyUniqueFIGIs.append(iData["figi"])
1410
1411        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1412
1413        return onlyUniqueFIGIs
1414
1415    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1416        """
1417        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1418
1419        See limits: https://tinkoff.github.io/investAPI/limits/
1420
1421        If `pricesFile` string is not empty then also save information to this file.
1422
1423        :param instruments: list of strings with tickers or FIGIs.
1424        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1425        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1427        """
1428        if instruments is None or not instruments:
1429            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1430            raise Exception("Ticker or FIGI required")
1431
1432        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1433
1434        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1435
1436        iList = []  # trying to get info and current prices about all unique instruments:
1437        for self.figi in onlyUniqueFIGIs:
1438            iData = self.SearchByFIGI(requestPrice=True)
1439            iList.append(iData)
1440
1441        self.ShowListOfPrices(iList, show)
1442
1443        return iList
1444
1445    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1446        """
1447        Show table contains current prices of given instruments.
1448
1449        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1450                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1451        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1452        :return: multilines text in Markdown format as a table contains current prices.
1453        """
1454        infoText = ""
1455
1456        if show or self.pricesFile:
1457            info = [
1458                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1459                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1460                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1461            ]
1462
1463            for item in iList:
1464                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1465                    item["ticker"],
1466                    item["figi"],
1467                    item["type"],
1468                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1469                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1470                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1471                    "{} / {}".format(
1472                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1473                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1474                    ),
1475                    "{} / {}".format(
1476                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1477                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1478                    ),
1479                    item["currency"],
1480                ))
1481
1482            infoText = "".join(info)
1483
1484            if show:
1485                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1486
1487            if self.pricesFile:
1488                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1489                    fH.write(infoText)
1490
1491                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1492
1493        return infoText
1494
1495    def RequestTradingStatus(self) -> dict:
1496        """
1497        Requesting trading status for the instrument defined by `figi` variable.
1498
1499        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1500
1501        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1502
1503        :return: dictionary with trading status attributes. Response example:
1504                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1505                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1506        """
1507        if self.figi is None or not self.figi:
1508            uLogger.error("Variable `figi` must be defined for using this method!")
1509            raise Exception("FIGI required")
1510
1511        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1512
1513        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1514        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1515        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1516
1517        if self.moreDebug:
1518            uLogger.debug("Records about current trading status successfully received")
1519
1520        return tradingStatus
1521
1522    def RequestPortfolio(self) -> dict:
1523        """
1524        Requesting actual user's portfolio for current `accountId`.
1525
1526        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1527
1528        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1529
1530        :return: dictionary with user's portfolio.
1531        """
1532        if self.accountId is None or not self.accountId:
1533            uLogger.error("Variable `accountId` must be defined for using this method!")
1534            raise Exception("Account ID required")
1535
1536        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1537
1538        self.body = str({"accountId": self.accountId})
1539        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1540        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1541
1542        if self.moreDebug:
1543            uLogger.debug("Records about user's portfolio successfully received")
1544
1545        return rawPortfolio
1546
1547    def RequestPositions(self) -> dict:
1548        """
1549        Requesting open positions by currencies and instruments for current `accountId`.
1550
1551        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1552
1553        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1554
1555        :return: dictionary with open positions by instruments.
1556        """
1557        if self.accountId is None or not self.accountId:
1558            uLogger.error("Variable `accountId` must be defined for using this method!")
1559            raise Exception("Account ID required")
1560
1561        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1562
1563        self.body = str({"accountId": self.accountId})
1564        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1565        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1566
1567        if self.moreDebug:
1568            uLogger.debug("Records about current open positions successfully received")
1569
1570        return rawPositions
1571
1572    def RequestPendingOrders(self) -> list:
1573        """
1574        Requesting current actual pending orders for current `accountId`.
1575
1576        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1577
1578        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1579
1580        :return: list of dictionaries with pending orders.
1581        """
1582        if self.accountId is None or not self.accountId:
1583            uLogger.error("Variable `accountId` must be defined for using this method!")
1584            raise Exception("Account ID required")
1585
1586        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1587
1588        self.body = str({"accountId": self.accountId})
1589        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1590        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1591
1592        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1593
1594        return rawOrders
1595
1596    def RequestStopOrders(self) -> list:
1597        """
1598        Requesting current actual stop orders for current `accountId`.
1599
1600        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1601
1602        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1603
1604        :return: list of dictionaries with stop orders.
1605        """
1606        if self.accountId is None or not self.accountId:
1607            uLogger.error("Variable `accountId` must be defined for using this method!")
1608            raise Exception("Account ID required")
1609
1610        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1611
1612        self.body = str({"accountId": self.accountId})
1613        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1614        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1615
1616        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1617
1618        return rawStopOrders
1619
1620    def Overview(self, show: bool = False, details: str = "full") -> dict:
1621        """
1622        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1623        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1624        and `overviewBondsCalendarFile` are defined then also save information to file.
1625
1626        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1627        many requests about the state of the portfolio, and then, based on the received data, a large number
1628        of calculation and statistics are collected.
1629
1630        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1631        :param details: how detailed should the information be?
1632        - `full` — shows full available information about portfolio status (by default),
1633        - `positions` — shows only open positions,
1634        - `orders` — shows only sections of open limits and stop orders.
1635        - `digest` — show a short digest of the portfolio status,
1636        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1637        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1638        :return: dictionary with client's raw portfolio and some statistics.
1639        """
1640        if self.accountId is None or not self.accountId:
1641            uLogger.error("Variable `accountId` must be defined for using this method!")
1642            raise Exception("Account ID required")
1643
1644        view = {
1645            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1646                "headers": {},  # list of dictionaries, response headers without "positions" section
1647                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1648                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1649                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1650                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1651                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1652                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1653                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1654                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1655                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1656            },
1657            "stat": {  # --- some statistics calculated using "raw" sections:
1658                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1659                "availableRUB": 0.,  # available rubles (without other currencies)
1660                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1661                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1662                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1663                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1664                "sharesCostRUB": 0.,  # costs of all shares in RUB
1665                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1666                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1667                "futuresCostRUB": 0.,  # costs of all futures in RUB
1668                "Currencies": [],  # list of dictionaries of all currencies statistics
1669                "Shares": [],  # list of dictionaries of all shares statistics
1670                "Bonds": [],  # list of dictionaries of all bonds statistics
1671                "Etfs": [],  # list of dictionaries of all etfs statistics
1672                "Futures": [],  # list of dictionaries of all futures statistics
1673                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1674                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1675                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1676                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1677                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1678            },
1679            "analytics": {  # --- some analytics of portfolio:
1680                "distrByAssets": {},  # portfolio distribution by assets
1681                "distrByCompanies": {},  # portfolio distribution by companies
1682                "distrBySectors": {},  # portfolio distribution by sectors
1683                "distrByCurrencies": {},  # portfolio distribution by currencies
1684                "distrByCountries": {},  # portfolio distribution by countries
1685                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1686            }
1687        }
1688
1689        details = details.lower()
1690        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1691        if details not in availableDetails:
1692            details = "full"
1693            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1694
1695        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1696
1697        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1698        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1699        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1700        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1701
1702        # save response headers without "positions" section:
1703        for key in portfolioResponse.keys():
1704            if key != "positions":
1705                view["raw"]["headers"][key] = portfolioResponse[key]
1706
1707            else:
1708                continue
1709
1710        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1711        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1712        for item in portfolioResponse["positions"]:
1713            if item["instrumentType"] == "currency":
1714                self.figi = item["figi"]
1715                curr = self.SearchByFIGI(requestPrice=False)
1716
1717                # current price of currency in RUB:
1718                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1719                    "name": curr["name"],
1720                    "currentPrice": NanoToFloat(
1721                        item["currentPrice"]["units"],
1722                        item["currentPrice"]["nano"]
1723                    ),
1724                }
1725
1726                view["raw"]["Currencies"].append(item)
1727
1728            elif item["instrumentType"] == "share":
1729                view["raw"]["Shares"].append(item)
1730
1731            elif item["instrumentType"] == "bond":
1732                view["raw"]["Bonds"].append(item)
1733
1734            elif item["instrumentType"] == "etf":
1735                view["raw"]["Etfs"].append(item)
1736
1737            elif item["instrumentType"] == "futures":
1738                view["raw"]["Futures"].append(item)
1739
1740            else:
1741                continue
1742
1743        # how many volume of currencies (by ISO currency name) are blocked:
1744        for item in view["raw"]["positions"]["blocked"]:
1745            blocked = NanoToFloat(item["units"], item["nano"])
1746            if blocked > 0:
1747                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1748
1749        # how many volume of instruments (by FIGI) are blocked:
1750        for item in view["raw"]["positions"]["securities"]:
1751            blocked = int(item["blocked"])
1752            if blocked > 0:
1753                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1754
1755        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1756
1757        if "rub" in allBlocked.keys():
1758            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1759
1760        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1761        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1762        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1763        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1764        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1765        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1766        view["stat"]["portfolioCostRUB"] = sum([
1767            view["stat"]["allCurrenciesCostRUB"],
1768            view["stat"]["sharesCostRUB"],
1769            view["stat"]["bondsCostRUB"],
1770            view["stat"]["etfsCostRUB"],
1771            view["stat"]["futuresCostRUB"],
1772        ])
1773
1774        # --- calculating some portfolio statistics:
1775        byComp = {}  # distribution by companies
1776        bySect = {}  # distribution by sectors
1777        byCurr = {}  # distribution by currencies (include RUB)
1778        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1779        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1780
1781        for item in portfolioResponse["positions"]:
1782            self.figi = item["figi"]
1783            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1784
1785            if instrument:
1786                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1787                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1788
1789                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1790                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1791
1792                else:
1793                    blocked = 0
1794
1795                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1796                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1797                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1798                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1799                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1800                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1801                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1802                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1803                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1804                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1805                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1806                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1807
1808                statData = {
1809                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1810                    "ticker": instrument["ticker"],  # ticker by FIGI
1811                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1812                    "volume": volume,  # available volume of instrument
1813                    "lots": lots,  # volume in lots of instrument
1814                    "direction": direction,  # direction of an instrument's position: short or long
1815                    "blocked": blocked,  # blocked volume of currency or instrument
1816                    "currentPrice": curPrice,  # current instrument's price in basic asset
1817                    "average": average,  # current average position price
1818                    "cost": cost,  # current cost of all volume of instrument in basic asset
1819                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1820                    "costRUB": costRUB,  # cost of instrument in ruble
1821                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1822                    "profit": profit,  # expected profit at current moment
1823                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1824                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1825                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1826                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1827                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1828                    "step": instrument["step"],  # minimum price increment
1829                }
1830
1831                # adding distribution by unique countries:
1832                if statData["country"] not in byCountry.keys():
1833                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1834
1835                else:
1836                    byCountry[statData["country"]]["cost"] += costRUB
1837                    byCountry[statData["country"]]["percent"] += percentCostRUB
1838
1839                if item["instrumentType"] != "currency":
1840                    # adding distribution by unique companies:
1841                    if statData["name"]:
1842                        if statData["name"] not in byComp.keys():
1843                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1844
1845                        else:
1846                            byComp[statData["name"]]["cost"] += costRUB
1847                            byComp[statData["name"]]["percent"] += percentCostRUB
1848
1849                    # adding distribution by unique sectors:
1850                    if statData["sector"] not in bySect.keys():
1851                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1852
1853                    else:
1854                        bySect[statData["sector"]]["cost"] += costRUB
1855                        bySect[statData["sector"]]["percent"] += percentCostRUB
1856
1857                # adding distribution by unique currencies:
1858                if currency not in byCurr.keys():
1859                    byCurr[currency] = {
1860                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1861                        "cost": costRUB,
1862                        "percent": percentCostRUB
1863                    }
1864
1865                else:
1866                    byCurr[currency]["cost"] += costRUB
1867                    byCurr[currency]["percent"] += percentCostRUB
1868
1869                # saving statistics for every instrument:
1870                if item["instrumentType"] == "currency":
1871                    view["stat"]["Currencies"].append(statData)
1872
1873                    # update dict with free funds for trading (total - blocked) by currencies
1874                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1875                    view["stat"]["funds"][currency] = {
1876                        "total": volume,
1877                        "totalCostRUB": costRUB,  # total volume cost in rubles
1878                        "free": volume - blocked,
1879                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1880                    }
1881
1882                elif item["instrumentType"] == "share":
1883                    view["stat"]["Shares"].append(statData)
1884
1885                elif item["instrumentType"] == "bond":
1886                    view["stat"]["Bonds"].append(statData)
1887
1888                elif item["instrumentType"] == "etf":
1889                    view["stat"]["Etfs"].append(statData)
1890
1891                elif item["instrumentType"] == "Futures":
1892                    view["stat"]["Futures"].append(statData)
1893
1894                else:
1895                    continue
1896
1897        # total changes in Russian Ruble:
1898        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1899        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1900        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1901        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1902        view["stat"]["funds"]["rub"] = {
1903            "total": view["stat"]["availableRUB"],
1904            "totalCostRUB": view["stat"]["availableRUB"],
1905            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1906            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907        }
1908
1909        # --- pending orders sector data:
1910        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1911        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1912
1913        for item in view["raw"]["orders"]:
1914            self.figi = item["figi"]
1915
1916            if item["figi"] not in uniquePendingOrdersFIGIs:
1917                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1918
1919                uniquePendingOrdersFIGIs.append(item["figi"])
1920                uniquePendingOrders[item["figi"]] = instrument
1921
1922            else:
1923                instrument = uniquePendingOrders[item["figi"]]
1924
1925            if instrument:
1926                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1927                orderType = TKS_ORDER_TYPES[item["orderType"]]
1928                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1929                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1930
1931                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1932                if item["direction"] == "ORDER_DIRECTION_BUY":
1933                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1934
1935                else:
1936                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1937
1938                # requested price for order execution:
1939                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1940
1941                # necessary changes in percent to reach target from current price:
1942                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1943
1944                view["stat"]["orders"].append({
1945                    "orderID": item["orderId"],  # orderId number parameter of current order
1946                    "figi": item["figi"],  # FIGI identification
1947                    "ticker": instrument["ticker"],  # ticker name by FIGI
1948                    "lotsRequested": item["lotsRequested"],  # requested lots value
1949                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1950                    "currentPrice": lastPrice,  # current instrument's price for defined action
1951                    "targetPrice": target,  # requested price for order execution in base currency
1952                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1953                    "percentChanges": changes,  # changes in percent to target from current price
1954                    "currency": item["currency"],  # instrument's currency name
1955                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1956                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1957                    "status": orderState,  # order status from TKS_ORDER_STATES
1958                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1959                })
1960
1961        # --- stop orders sector data:
1962        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1963        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1964
1965        for item in view["raw"]["stopOrders"]:
1966            self.figi = item["figi"]
1967
1968            if item["figi"] not in uniqueStopOrdersFIGIs:
1969                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1970
1971                uniqueStopOrdersFIGIs.append(item["figi"])
1972                uniqueStopOrders[item["figi"]] = instrument
1973
1974            else:
1975                instrument = uniqueStopOrders[item["figi"]]
1976
1977            if instrument:
1978                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1979                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1980                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1981
1982                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1983                if "expirationTime" in item.keys():
1984                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1985                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1986
1987                else:
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1989                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1990
1991                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1992                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1993                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1994
1995                else:
1996                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1997
1998                # requested price when stop-order executed:
1999                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2000
2001                # price for limit-order, set up when stop-order executed:
2002                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2003
2004                # necessary changes in percent to reach target from current price:
2005                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2006
2007                view["stat"]["stopOrders"].append({
2008                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2009                    "figi": item["figi"],  # FIGI identification
2010                    "ticker": instrument["ticker"],  # ticker name by FIGI
2011                    "lotsRequested": item["lotsRequested"],  # requested lots value
2012                    "currentPrice": lastPrice,  # current instrument's price for defined action
2013                    "targetPrice": target,  # requested price for stop-order execution in base currency
2014                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2015                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2016                    "percentChanges": changes,  # changes in percent to target from current price
2017                    "currency": item["currency"],  # instrument's currency name
2018                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2019                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2020                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2021                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2022                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2023                })
2024
2025        # --- calculating data for analytics section:
2026        # portfolio distribution by assets:
2027        view["analytics"]["distrByAssets"] = {
2028            "Ruble": {
2029                "uniques": 1,
2030                "cost": view["stat"]["availableRUB"],
2031                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2032            },
2033            "Currencies": {
2034                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2035                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2036                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2037            },
2038            "Shares": {
2039                "uniques": len(view["stat"]["Shares"]),
2040                "cost": view["stat"]["sharesCostRUB"],
2041                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042            },
2043            "Bonds": {
2044                "uniques": len(view["stat"]["Bonds"]),
2045                "cost": view["stat"]["bondsCostRUB"],
2046                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2047            },
2048            "Etfs": {
2049                "uniques": len(view["stat"]["Etfs"]),
2050                "cost": view["stat"]["etfsCostRUB"],
2051                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2052            },
2053            "Futures": {
2054                "uniques": len(view["stat"]["Futures"]),
2055                "cost": view["stat"]["futuresCostRUB"],
2056                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057            },
2058        }
2059
2060        # portfolio distribution by companies:
2061        view["analytics"]["distrByCompanies"]["All money cash"] = {
2062            "ticker": "",
2063            "cost": view["stat"]["allCurrenciesCostRUB"],
2064            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2065        }
2066        view["analytics"]["distrByCompanies"].update(byComp)
2067
2068        # portfolio distribution by sectors:
2069        view["analytics"]["distrBySectors"]["All money cash"] = {
2070            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2071            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2072        }
2073        view["analytics"]["distrBySectors"].update(bySect)
2074
2075        # portfolio distribution by currencies:
2076        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2077            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2078
2079            if self.moreDebug:
2080                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2081
2082        view["analytics"]["distrByCurrencies"].update(byCurr)
2083        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2084        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2085
2086        # portfolio distribution by countries:
2087        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2088            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2089
2090            if self.moreDebug:
2091                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2092
2093        view["analytics"]["distrByCountries"].update(byCountry)
2094        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2096
2097        # --- Prepare text statistics overview in human-readable:
2098        if show:
2099            # Whatever the value `details`, header not changes:
2100            info = [
2101                "# Client's portfolio\n\n",
2102                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2103                "* **Account ID:** [{}]\n".format(self.accountId),
2104            ]
2105
2106            if details in ["full", "positions", "digest"]:
2107                info.extend([
2108                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2109                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2110                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2111                        view["stat"]["totalChangesRUB"],
2112                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2113                        view["stat"]["totalChangesPercentRUB"],
2114                    ),
2115                ])
2116
2117            if details in ["full", "positions"]:
2118                info.extend([
2119                    "## Open positions\n\n",
2120                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2121                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2122                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2123                        "{:.2f} ({:.2f}) rub".format(
2124                            view["stat"]["availableRUB"],
2125                            view["stat"]["blockedRUB"],
2126                        )
2127                    )
2128                ])
2129
2130                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2131                    return [
2132                        "|                             |                                 |          |              |              |                     |                              |\n",
2133                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2134                            noTradeStr if noTradeStr else typeStr,
2135                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2136                        ),
2137                    ]
2138
2139                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2140                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2141                        "{} [{}]".format(data["ticker"], data["figi"]),
2142                        "{:.2f} ({:.2f}) {}".format(
2143                            data["volume"],
2144                            data["blocked"],
2145                            data["currency"],
2146                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2147                            data["volume"],
2148                            data["blocked"],
2149                        ),
2150                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2151                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2152                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2153                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2154                        "{}{:.2f} {} ({}{:.2f}%)".format(
2155                            "+" if data["profit"] > 0 else "",
2156                            data["profit"], data["baseCurrencyName"],
2157                            "+" if data["percentProfit"] > 0 else "",
2158                            data["percentProfit"],
2159                        ),
2160                    )
2161
2162                # --- Show currencies section:
2163                if view["stat"]["Currencies"]:
2164                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2165                    for item in view["stat"]["Currencies"]:
2166                        info.append(_InfoStr(item, showCurrencyName=True))
2167
2168                else:
2169                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2170
2171                # --- Show shares section:
2172                if view["stat"]["Shares"]:
2173                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2174
2175                    for item in view["stat"]["Shares"]:
2176                        info.append(_InfoStr(item))
2177
2178                else:
2179                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2180
2181                # --- Show bonds section:
2182                if view["stat"]["Bonds"]:
2183                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2184
2185                    for item in view["stat"]["Bonds"]:
2186                        info.append(_InfoStr(item))
2187
2188                else:
2189                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2190
2191                # --- Show etfs section:
2192                if view["stat"]["Etfs"]:
2193                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2194
2195                    for item in view["stat"]["Etfs"]:
2196                        info.append(_InfoStr(item))
2197
2198                else:
2199                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2200
2201                # --- Show futures section:
2202                if view["stat"]["Futures"]:
2203                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2204
2205                    for item in view["stat"]["Futures"]:
2206                        info.append(_InfoStr(item))
2207
2208                else:
2209                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2210
2211            if details in ["full", "orders"]:
2212                # --- Show pending orders section:
2213                if view["stat"]["orders"]:
2214                    info.extend([
2215                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2216                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2217                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2218                    ])
2219
2220                    for item in view["stat"]["orders"]:
2221                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2222                            "{} [{}]".format(item["ticker"], item["figi"]),
2223                            item["orderID"],
2224                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2225                            "{} {} ({}{:.2f}%)".format(
2226                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2227                                item["baseCurrencyName"],
2228                                "+" if item["percentChanges"] > 0 else "",
2229                                float(item["percentChanges"]),
2230                            ),
2231                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2232                            item["action"],
2233                            item["type"],
2234                            item["date"],
2235                        ))
2236
2237                else:
2238                    info.append("\n## Total pending limit-orders: 0\n")
2239
2240                # --- Show stop orders section:
2241                if view["stat"]["stopOrders"]:
2242                    info.extend([
2243                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2244                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2245                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2246                    ])
2247
2248                    for item in view["stat"]["stopOrders"]:
2249                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2250                            "{} [{}]".format(item["ticker"], item["figi"]),
2251                            item["orderID"],
2252                            item["lotsRequested"],
2253                            "{} {} ({}{:.2f}%)".format(
2254                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2255                                item["baseCurrencyName"],
2256                                "+" if item["percentChanges"] > 0 else "",
2257                                float(item["percentChanges"]),
2258                            ),
2259                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2260                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2261                            item["action"],
2262                            item["type"],
2263                            item["expType"],
2264                            item["createDate"],
2265                            item["expDate"],
2266                        ))
2267
2268                else:
2269                    info.append("\n## Total stop-orders: 0\n")
2270
2271            if details in ["full", "analytics"]:
2272                # -- Show analytics section:
2273                if view["stat"]["portfolioCostRUB"] > 0:
2274                    info.extend([
2275                        "\n# Analytics\n"
2276                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2277                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2278                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2279                            view["stat"]["totalChangesRUB"],
2280                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2281                            view["stat"]["totalChangesPercentRUB"],
2282                        ),
2283                        "\n## Portfolio distribution by assets\n"
2284                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2285                        "|------------------------------------|---------|---------|--------------------|\n",
2286                    ])
2287
2288                    for key in view["analytics"]["distrByAssets"].keys():
2289                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2290                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2291                                key,
2292                                view["analytics"]["distrByAssets"][key]["uniques"],
2293                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2294                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2295                            ))
2296
2297                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2298
2299                    info.extend([
2300                        "\n## Portfolio distribution by companies\n"
2301                        "\n| Company                                      | Percent | Current cost       |\n",
2302                        aSepLine,
2303                    ])
2304
2305                    for company in view["analytics"]["distrByCompanies"].keys():
2306                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2307                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2308                                "{}{}".format(
2309                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2310                                    company,
2311                                ),
2312                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2313                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2314                            ))
2315
2316                    info.extend([
2317                        "\n## Portfolio distribution by sectors\n"
2318                        "\n| Sector                                       | Percent | Current cost       |\n",
2319                        aSepLine,
2320                    ])
2321
2322                    for sector in view["analytics"]["distrBySectors"].keys():
2323                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2324                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2325                                sector,
2326                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2327                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2328                            ))
2329
2330                    info.extend([
2331                        "\n## Portfolio distribution by currencies\n"
2332                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2333                        aSepLine,
2334                    ])
2335
2336                    for curr in view["analytics"]["distrByCurrencies"].keys():
2337                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2338                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2339                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2340                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2341                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2342                            ))
2343
2344                    info.extend([
2345                        "\n## Portfolio distribution by countries\n"
2346                        "\n| Assets by country                            | Percent | Current cost       |\n",
2347                        aSepLine,
2348                    ])
2349
2350                    for country in view["analytics"]["distrByCountries"].keys():
2351                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2352                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2353                                country,
2354                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2355                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2356                            ))
2357
2358            if details in ["full", "calendar"]:
2359                # -- Show bonds payment calendar section:
2360                if view["stat"]["Bonds"]:
2361                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2362                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2363                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2364
2365                else:
2366                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2367
2368            infoText = "".join(info)
2369
2370            uLogger.info(infoText)
2371
2372            if details == "full" and self.overviewFile:
2373                filename = self.overviewFile
2374
2375            elif details == "digest" and self.overviewDigestFile:
2376                filename = self.overviewDigestFile
2377
2378            elif details == "positions" and self.overviewPositionsFile:
2379                filename = self.overviewPositionsFile
2380
2381            elif details == "orders" and self.overviewOrdersFile:
2382                filename = self.overviewOrdersFile
2383
2384            elif details == "analytics" and self.overviewAnalyticsFile:
2385                filename = self.overviewAnalyticsFile
2386
2387            elif details == "calendar" and self.overviewBondsCalendarFile:
2388                filename = self.overviewBondsCalendarFile
2389
2390            else:
2391                filename = ""
2392
2393            if filename:
2394                with open(filename, "w", encoding="UTF-8") as fH:
2395                    fH.write(infoText)
2396
2397                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2398
2399        return view
2400
2401    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2402        """
2403        Returns history operations between two given dates for current `accountId`.
2404        If `reportFile` string is not empty then also save human-readable report.
2405        Shows some statistical data of closed positions.
2406
2407        :param start: see docstring in `GetDatesAsString()` method
2408        :param end: see docstring in `GetDatesAsString()` method
2409        :param show: if `True` then also prints all records to the console.
2410        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2411        :return: original list of dictionaries with history of deals records from API ("operations" key):
2412                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2413                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2414        """
2415        if self.accountId is None or not self.accountId:
2416            uLogger.error("Variable `accountId` must be defined for using this method!")
2417            raise Exception("Account ID required")
2418
2419        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2420
2421        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2422
2423        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2424        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2425        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2426        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2427        customStat = {}  # custom statistics in additional to responseJSON
2428
2429        # --- output report in human-readable format:
2430        if show or self.reportFile:
2431            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2432            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2433            nextDay = ""
2434
2435            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2436
2437            if len(ops) > 0:
2438                customStat = {
2439                    "opsCount": 0,  # total operations count
2440                    "buyCount": 0,  # buy operations
2441                    "sellCount": 0,  # sell operations
2442                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2443                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2444                    "payIn": {"rub": 0.},  # Deposit brokerage account
2445                    "payOut": {"rub": 0.},  # Withdrawals
2446                    "divs": {"rub": 0.},  # Dividends income
2447                    "coupons": {"rub": 0.},  # Coupon's income
2448                    "brokerCom": {"rub": 0.},  # Service commissions
2449                    "serviceCom": {"rub": 0.},  # Service commissions
2450                    "marginCom": {"rub": 0.},  # Margin commissions
2451                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2452                }
2453
2454                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2455                for item in ops:
2456                    if item["state"] == "OPERATION_STATE_EXECUTED":
2457                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2458
2459                        # count buy operations:
2460                        if "_BUY" in item["operationType"]:
2461                            customStat["buyCount"] += 1
2462
2463                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2464                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2468
2469                        # count sell operations:
2470                        elif "_SELL" in item["operationType"]:
2471                            customStat["sellCount"] += 1
2472
2473                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2474                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2478
2479                        # count incoming operations:
2480                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2481                            if item["payment"]["currency"] in customStat["payIn"].keys():
2482                                customStat["payIn"][item["payment"]["currency"]] += payment
2483
2484                            else:
2485                                customStat["payIn"][item["payment"]["currency"]] = payment
2486
2487                        # count withdrawals operations:
2488                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2489                            if item["payment"]["currency"] in customStat["payOut"].keys():
2490                                customStat["payOut"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["payOut"][item["payment"]["currency"]] = payment
2494
2495                        # count dividends income:
2496                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2497                            if item["payment"]["currency"] in customStat["divs"].keys():
2498                                customStat["divs"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["divs"][item["payment"]["currency"]] = payment
2502
2503                        # count coupon's income:
2504                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2505                            if item["payment"]["currency"] in customStat["coupons"].keys():
2506                                customStat["coupons"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["coupons"][item["payment"]["currency"]] = payment
2510
2511                        # count broker commissions:
2512                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2513                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2514                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2518
2519                        # count service commissions:
2520                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2521                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2522                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2526
2527                        # count margin commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2529                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2530                                customStat["marginCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["marginCom"][item["payment"]["currency"]] = payment
2534
2535                        # count withholding taxes:
2536                        elif "_TAX" in item["operationType"]:
2537                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2538                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2542
2543                        else:
2544                            continue
2545
2546                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2547
2548                # --- view "Actions" lines:
2549                info.extend([
2550                    "| Report sections            |                               |                              |                      |                        |\n",
2551                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2552                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2553                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2554                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2555                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2556                    ),
2557                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2558                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2559                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2560                    ),
2561                ])
2562
2563                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2564                for key in opsKeys:
2565                    if key == "rub":
2566                        continue
2567
2568                    info.extend([
2569                        "|                            |                               | {:<28} |                      |                        |\n".format(
2570                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2571                        ),
2572                        "|                            |                               | {:<28} |                      |                        |\n".format(
2573                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2574                        ),
2575                    ])
2576
2577                info.append(splitLine1)
2578
2579                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2580                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2581                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2582                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2583                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2584                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2585                    )
2586
2587                # --- view "Payments" lines:
2588                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2589                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2590
2591                for key in paymentsKeys:
2592                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2593
2594                info.append(splitLine1)
2595
2596                # --- view "Commissions and taxes" lines:
2597                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2598                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2599
2600                for key in comKeys:
2601                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2602
2603                info.append(splitLine1)
2604
2605                info.extend([
2606                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2607                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2608                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2609                ])
2610
2611            else:
2612                info.append("Broker returned no operations during this period\n")
2613
2614            # --- view "Operations" section:
2615            for item in ops:
2616                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2617                    continue
2618
2619                else:
2620                    self.figi = item["figi"] if item["figi"] else ""
2621                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2622                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2623
2624                    # group of deals during one day:
2625                    if nextDay and item["date"].split("T")[0] != nextDay:
2626                        info.append(splitLine2)
2627                        nextDay = ""
2628
2629                    else:
2630                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2631
2632                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2633                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2634                        self.figi if self.figi else "—",
2635                        instrument["ticker"] if instrument else "—",
2636                        instrument["type"] if instrument else "—",
2637                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2638                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2639                        TKS_OPERATION_STATES[item["state"]],
2640                        TKS_OPERATION_TYPES[item["operationType"]],
2641                    ))
2642
2643            infoText = "".join(info)
2644
2645            if show:
2646                if self.moreDebug:
2647                    uLogger.debug("Records about history of a client's operations successfully received")
2648
2649                uLogger.info(infoText)
2650
2651            if self.reportFile:
2652                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2653                    fH.write(infoText)
2654
2655                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2656
2657        return ops, customStat
2658
2659    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2660        """
2661        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2662
2663        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2664        Warning! Broker server used ISO UTC time by default.
2665
2666        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2667        Also, `historyFile` used to update history with `onlyMissing` parameter.
2668
2669        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2670
2671        :param start: see docstring in `GetDatesAsString()` method.
2672        :param end: see docstring in `GetDatesAsString()` method.
2673        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2674                         `"hour"`, `"day"`. Default: `"hour"`.
2675        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2676                            False by default. Warning! History appends only from last candle to current time
2677                            with always update last candle!
2678        :param csvSep: separator if csv-file is used, `,` by default.
2679        :param show: if `True` then also prints Pandas DataFrame to the console.
2680        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2681                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2682        """
2683        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2684        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2685        history = None  # empty pandas object for history
2686
2687        if interval not in TKS_CANDLE_INTERVALS.keys():
2688            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2689            raise Exception("Incorrect value")
2690
2691        if not (self.ticker or self.figi):
2692            uLogger.error("Ticker or FIGI must be defined!")
2693            raise Exception("Ticker or FIGI required")
2694
2695        if self.ticker and not self.figi:
2696            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2697            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2698
2699        if self.figi and not self.ticker:
2700            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2701            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2702
2703        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2704        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2705        if interval.lower() != "day":
2706            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2707
2708        delta = dtEnd - dtStart  # current UTC time minus last time in file
2709        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2710
2711        # calculate history length in candles:
2712        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2713        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2714            length += 1  # to avoid fraction time
2715
2716        # calculate data blocks count:
2717        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2718
2719        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2720        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2721        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2722        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2723        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2724
2725        tempOld = None  # pandas object for old history, if --only-missing key present
2726        lastTime = None  # datetime object of last old candle in file
2727
2728        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2729            uLogger.debug("--only-missing key present, add only last missing candles...")
2730            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2731
2732            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2733
2734            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2735            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2736            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2737            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2738
2739            # get last datetime object from last string in file or minus 1 delta if file is empty:
2740            if len(tempOld) > 0:
2741                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2742
2743            else:
2744                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2745
2746            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2747
2748        responseJSONs = []  # raw history blocks of data
2749
2750        blockEnd = dtEnd
2751        for item in range(blocks):
2752            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2753            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2754
2755            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2756                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2757            ))
2758
2759            if blockStart == blockEnd:
2760                uLogger.debug("Skipped this zero-length block...")
2761
2762            else:
2763                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2764                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2765                self.body = str({
2766                    "figi": self.figi,
2767                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2768                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2769                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2770                })
2771                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2772
2773                if "code" in responseJSON.keys():
2774                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2775
2776                else:
2777                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2778                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2779
2780                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2781
2782            blockEnd = blockStart
2783
2784        printCount = len(responseJSONs)  # candles to show in console
2785        if responseJSONs:
2786            tempHistory = pd.DataFrame(
2787                data={
2788                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2789                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2790                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2791                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2792                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2793                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2794                    "volume": [int(item["volume"]) for item in responseJSONs],
2795                },
2796                index=range(len(responseJSONs)),
2797                columns=["date", "time", "open", "high", "low", "close", "volume"],
2798            )
2799            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2800            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2801
2802            # append only newest candles to old history if --only-missing key present:
2803            if onlyMissing and tempOld is not None and lastTime is not None:
2804                index = 0  # find start index in tempHistory data:
2805
2806                for i, item in tempHistory.iterrows():
2807                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2808
2809                    if curTime == lastTime:
2810                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2811                        index = i
2812                        printCount = index + 1
2813                        break
2814
2815                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2816
2817            else:
2818                history = tempHistory  # if no `--only-missing` key then load full data from server
2819
2820            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2821
2822        if history is not None and not history.empty:
2823            if show:
2824                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2825                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2826                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2827                ))
2828
2829        else:
2830            uLogger.warning("Received an empty candles history!")
2831
2832        if self.historyFile is not None:
2833            if history is not None and not history.empty:
2834                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2835                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2836
2837            else:
2838                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2839
2840        else:
2841            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2842
2843        return history
2844
2845    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2846        """
2847        Load candles history from csv-file and return Pandas DataFrame object.
2848
2849        See also: `History()` and `ShowHistoryChart()` methods.
2850
2851        :param filePath: path to csv-file to open.
2852        """
2853        loadedHistory = None  # init candles data object
2854
2855        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2856
2857        if os.path.exists(filePath):
2858            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2859
2860            tfStr = self.priceModel.FormattedDelta(
2861                self.priceModel.timeframe,
2862                "{days} days {hours}h {minutes}m {seconds}s",
2863            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2864                self.priceModel.timeframe,
2865                "{hours}h {minutes}m {seconds}s",
2866            )
2867
2868            if loadedHistory is not None and not loadedHistory.empty:
2869                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2870                    len(loadedHistory),
2871                    tfStr,
2872                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2873                )
2874
2875            else:
2876                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2877
2878        else:
2879            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2880
2881        return loadedHistory
2882
2883    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2884        """
2885        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2886
2887        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2888        Default: `index.html` (both for interact and non-interact candlesticks chart).
2889
2890        See also: `History()` and `LoadHistory()` methods.
2891
2892        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2893        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2894                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2895                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2896                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2897        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2898                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2899        """
2900        if isinstance(candles, str):
2901            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2902            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2903
2904        elif isinstance(candles, pd.DataFrame):
2905            self.priceModel.prices = candles  # set candles chain from variable
2906            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2907
2908            if "datetime" not in candles.columns:
2909                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2910
2911        else:
2912            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2913            raise Exception("Incorrect value")
2914
2915        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2916
2917        if interact:
2918            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2919
2920            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2921
2922        else:
2923            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2924
2925            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2926
2927        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2928
2929    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2930        """
2931        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2932        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2933
2934        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2935
2936        :param operation: string "Buy" or "Sell".
2937        :param lots: volume, integer count of lots >= 1.
2938        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2939        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2940        :param expDate: string "Undefined" by default or local date in future,
2941                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2942        :return: JSON with response from broker server.
2943        """
2944        if self.accountId is None or not self.accountId:
2945            uLogger.error("Variable `accountId` must be defined for using this method!")
2946            raise Exception("Account ID required")
2947
2948        if operation is None or not operation or operation not in ("Buy", "Sell"):
2949            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2950            raise Exception("Incorrect value")
2951
2952        if lots is None or lots < 1:
2953            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2954            lots = 1
2955
2956        if tp is None or tp < 0:
2957            tp = 0
2958
2959        if sl is None or sl < 0:
2960            sl = 0
2961
2962        if expDate is None or not expDate:
2963            expDate = "Undefined"
2964
2965        if not (self.ticker or self.figi):
2966            uLogger.error("Ticker or FIGI must be defined!")
2967            raise Exception("Ticker or FIGI required")
2968
2969        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2970        self.ticker = instrument["ticker"]
2971        self.figi = instrument["figi"]
2972
2973        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2974
2975        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2976        self.body = str({
2977            "figi": self.figi,
2978            "quantity": str(lots),
2979            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2980            "accountId": str(self.accountId),
2981            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2982        })
2983        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2984
2985        if "orderId" in response.keys():
2986            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2987                operation, response["orderId"],
2988                self.ticker, self.figi, lots,
2989                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2990                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2991                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2992            ))
2993
2994            if tp > 0:
2995                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2996
2997            if sl > 0:
2998                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2999
3000        else:
3001            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3002
3003        return response
3004
3005    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3006        """
3007        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3008        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3009
3010        See also: `Order()` and `Trade()` docstrings.
3011
3012        :param lots: volume, integer count of lots >= 1.
3013        :param tp: float > 0, take profit price of stop-order.
3014        :param sl: float > 0, stop loss price of stop-order.
3015        :param expDate: it's a local date in future.
3016                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3017        :return: JSON with response from broker server.
3018        """
3019        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3020
3021    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3022        """
3023        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3024        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3025
3026        See also: `Order()` and `Trade()` docstrings.
3027
3028        :param lots: volume, integer count of lots >= 1.
3029        :param tp: float > 0, take profit price of stop-order.
3030        :param sl: float > 0, stop loss price of stop-order.
3031        :param expDate: it's a local date in the future.
3032                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3033        :return: JSON with response from broker server.
3034        """
3035        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3036
3037    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3038        """
3039        Close position of given instruments.
3040
3041        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3042        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3043                         This avoids unnecessary downloading data from the server.
3044        """
3045        if instruments is None or not instruments:
3046            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3047            raise Exception("Ticker or FIGI required")
3048
3049        if isinstance(instruments, str):
3050            instruments = [instruments]
3051
3052        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3053        if uniqueInstruments:
3054            if portfolio is None or not portfolio:
3055                portfolio = self.Overview(show=False)
3056
3057            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3058            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3059
3060            for self.figi in uniqueInstruments:
3061                if self.figi not in allOpened:
3062                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3063                    continue
3064
3065                # search open trade info about instrument by ticker:
3066                instrument = {}
3067                for iType in TKS_INSTRUMENTS:
3068                    if instrument:
3069                        break
3070
3071                    for item in portfolio["stat"][iType]:
3072                        if item["figi"] == self.figi:
3073                            instrument = item
3074                            break
3075
3076                if instrument:
3077                    self.ticker = instrument["ticker"]
3078                    self.figi = instrument["figi"]
3079
3080                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3081                        self.ticker,
3082                        self.figi,
3083                        int(instrument["volume"]),
3084                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3085                    ))
3086
3087                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3088
3089                    if tradeLots > 0:
3090                        if instrument["blocked"] > 0:
3091                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3092                                instrument["blocked"],
3093                                self.ticker,
3094                                tradeLots,
3095                            ))
3096
3097                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3098                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3099
3100                    else:
3101                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3102
3103    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3104        """
3105        Close all positions of given instruments with defined type.
3106
3107        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3108        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3109                         This avoids unnecessary downloading data from the server.
3110        """
3111        if iType not in TKS_INSTRUMENTS:
3112            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3113
3114        else:
3115            if portfolio is None or not portfolio:
3116                portfolio = self.Overview(show=False)
3117
3118            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3119            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3120
3121            if tickers and portfolio:
3122                self.CloseTrades(tickers, portfolio)
3123
3124            else:
3125                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3126
3127    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3128        """
3129        Universal method to create market or limit orders with all available parameters for current `accountId`.
3130        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3131
3132        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3133        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3134
3135        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3136        then broker immediately open market order as you can do simple --buy or --sell operations!
3137
3138        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3139        When current price will go up or down to target price value then broker opens a limit order.
3140        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3141
3142        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3143
3144        :param operation: string "Buy" or "Sell".
3145        :param orderType: string "Limit" or "Stop".
3146        :param lots: volume, integer count of lots >= 1.
3147        :param targetPrice: target price > 0. This is open trade price for limit order.
3148        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3149                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3150        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3151                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3152                         Stop loss order always executed by market price.
3153        :param expDate: string "Undefined" by default or local date in future.
3154                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3155                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3156                        A limit order has no expiration date, it lasts until the end of the trading day.
3157        :return: JSON with response from broker server.
3158        """
3159        if self.accountId is None or not self.accountId:
3160            uLogger.error("Variable `accountId` must be defined for using this method!")
3161            raise Exception("Account ID required")
3162
3163        if operation is None or not operation or operation not in ("Buy", "Sell"):
3164            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3165            raise Exception("Incorrect value")
3166
3167        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3168            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3169            raise Exception("Incorrect value")
3170
3171        if lots is None or lots < 1:
3172            uLogger.error("You must define trade volume > 0: integer count of lots!")
3173            raise Exception("Incorrect value")
3174
3175        if targetPrice is None or targetPrice <= 0:
3176            uLogger.error("Target price for limit-order must be greater than 0!")
3177            raise Exception("Incorrect value")
3178
3179        if limitPrice is None or limitPrice <= 0:
3180            limitPrice = targetPrice
3181
3182        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3183            stopType = "Limit"
3184
3185        if expDate is None or not expDate:
3186            expDate = "Undefined"
3187
3188        if not (self.ticker or self.figi):
3189            uLogger.error("Tocker or FIGI must be defined!")
3190            raise Exception("Ticker or FIGI required")
3191
3192        response = {}
3193        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3194        self.ticker = instrument["ticker"]
3195        self.figi = instrument["figi"]
3196
3197        if orderType == "Limit":
3198            uLogger.debug(
3199                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3200                    self.ticker, self.figi,
3201                    operation, lots, targetPrice, instrument["currency"],
3202                ))
3203
3204            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3205            self.body = str({
3206                "figi": self.figi,
3207                "quantity": str(lots),
3208                "price": FloatToNano(targetPrice),
3209                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3210                "accountId": str(self.accountId),
3211                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3212            })
3213            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3214
3215            if "orderId" in response.keys():
3216                uLogger.info(
3217                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3218                        response["orderId"],
3219                        self.ticker, self.figi,
3220                        operation, lots, targetPrice, instrument["currency"],
3221                    ))
3222
3223                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3224                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3225                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3226                            targetPrice, instrument["currency"],
3227                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3228                        ))
3229
3230                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3231                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3232                            targetPrice, instrument["currency"],
3233                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3234                        ))
3235
3236            else:
3237                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3238
3239        if orderType == "Stop":
3240            uLogger.debug(
3241                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3242                    self.ticker, self.figi,
3243                    operation, lots,
3244                    targetPrice, instrument["currency"],
3245                    limitPrice, instrument["currency"],
3246                    stopType, expDate,
3247                ))
3248
3249            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3250            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3251            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3252
3253            body = {
3254                "figi": self.figi,
3255                "quantity": str(lots),
3256                "price": FloatToNano(limitPrice),
3257                "stopPrice": FloatToNano(targetPrice),
3258                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3259                "accountId": str(self.accountId),
3260                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3261                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3262            }
3263
3264            if expDateUTC:
3265                body["expireDate"] = expDateUTC
3266
3267            self.body = str(body)
3268            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3269
3270            if "stopOrderId" in response.keys():
3271                uLogger.info(
3272                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3273                        response["stopOrderId"],
3274                        self.ticker, self.figi,
3275                        operation, lots,
3276                        targetPrice, instrument["currency"],
3277                        limitPrice, instrument["currency"],
3278                        TKS_STOP_ORDER_TYPES[stopOrderType],
3279                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3280                    ))
3281
3282                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3283                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3284                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3285                            targetPrice, instrument["currency"],
3286                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3287                        ))
3288
3289                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3290                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3291                            targetPrice, instrument["currency"],
3292                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3293                        ))
3294
3295            else:
3296                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3297
3298        return response
3299
3300    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3301        """
3302        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3303        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3304        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3305        See also: `Order()` docstring.
3306
3307        :param lots: volume, integer count of lots >= 1.
3308        :param targetPrice: target price > 0. This is open trade price for limit order.
3309        :return: JSON with response from broker server.
3310        """
3311        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3312
3313    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3314        """
3315        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3316        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3317        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3318        target price value then broker opens a limit order. See also: `Order()` docstring.
3319
3320        :param lots: volume, integer count of lots >= 1.
3321        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3322        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3323                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3324        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3325                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3326        :param expDate: string "Undefined" by default or local date in future.
3327                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3328                        This date is converting to UTC format for server.
3329        :return: JSON with response from broker server.
3330        """
3331        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3332
3333    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3334        """
3335        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3336        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3337        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3338        See also: `Order()` docstring.
3339
3340        :param lots: volume, integer count of lots >= 1.
3341        :param targetPrice: target price > 0. This is open trade price for limit order.
3342        :return: JSON with response from broker server.
3343        """
3344        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3345
3346    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3347        """
3348        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3349        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3350        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3351        target price value then broker opens a limit order. See also: `Order()` docstring.
3352
3353        :param lots: volume, integer count of lots >= 1.
3354        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3355        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3356                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3357        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3358                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3359        :param expDate: string "Undefined" by default or local date in future.
3360                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3361                        This date is converting to UTC format for server.
3362        :return: JSON with response from broker server.
3363        """
3364        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3365
3366    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3367        """
3368        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3369
3370        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3371        :param allOrdersIDs: pre-received lists of all active pending orders.
3372                             This avoids unnecessary downloading data from the server.
3373        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3374        """
3375        if self.accountId is None or not self.accountId:
3376            uLogger.error("Variable `accountId` must be defined for using this method!")
3377            raise Exception("Account ID required")
3378
3379        if orderIDs:
3380            if allOrdersIDs is None or not allOrdersIDs:
3381                rawOrders = self.RequestPendingOrders()
3382                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3383
3384            if allStopOrdersIDs is None or not allStopOrdersIDs:
3385                rawStopOrders = self.RequestStopOrders()
3386                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3387
3388            for orderID in orderIDs:
3389                idInPendingOrders = orderID in allOrdersIDs
3390                idInStopOrders = orderID in allStopOrdersIDs
3391
3392                if not (idInPendingOrders or idInStopOrders):
3393                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3394                    continue
3395
3396                else:
3397                    if idInPendingOrders:
3398                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3399
3400                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3401                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3402                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3403                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3404
3405                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3406                            if self.moreDebug:
3407                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3408
3409                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3410
3411                        else:
3412                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3413
3414                    elif idInStopOrders:
3415                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3416
3417                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3418                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3419                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3420                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3421
3422                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3423                            if self.moreDebug:
3424                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3425
3426                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3427
3428                        else:
3429                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3430
3431                    else:
3432                        continue
3433
3434    def CloseAllOrders(self) -> None:
3435        """
3436        Gets a list of open pending and stop orders and cancel it all.
3437        """
3438        rawOrders = self.RequestPendingOrders()
3439        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3440        lenOrders = len(allOrdersIDs)
3441
3442        rawStopOrders = self.RequestStopOrders()
3443        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3444        lenSOrders = len(allStopOrdersIDs)
3445
3446        if lenOrders > 0 or lenSOrders > 0:
3447            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3448
3449            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3450
3451        else:
3452            uLogger.info("Orders not found, nothing to cancel.")
3453
3454    def CloseAll(self, *args) -> None:
3455        """
3456        Close all available (not blocked) opened trades and orders.
3457
3458        Also, you can select one or more keywords case-insensitive:
3459        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3460
3461        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3462        """
3463        overview = self.Overview(show=False)  # get all open trades info
3464
3465        if len(args) == 0:
3466            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3467            self.CloseAllOrders()  # close all pending and stop orders
3468
3469            for iType in TKS_INSTRUMENTS:
3470                if iType != "Currencies":
3471                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3472
3473        else:
3474            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3475            lowerArgs = [x.lower() for x in args]
3476
3477            if "orders" in lowerArgs:
3478                self.CloseAllOrders()  # close all pending and stop orders
3479
3480            for iType in TKS_INSTRUMENTS:
3481                if iType.lower() in lowerArgs and iType != "Currencies":
3482                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3483
3484    @staticmethod
3485    def ParseOrderParameters(operation, **inputParameters):
3486        """
3487        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3488
3489        :param operation: string "Buy" or "Sell".
3490        :param inputParameters: this is dict of strings that looks like this
3491               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3492               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3493               "prices" key: one or more prices to open limit-orders
3494               Counts of values in lots and prices lists must be equals!
3495        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3496        """
3497        # TODO: update order grid work with api v2
3498        pass
3499        # uLogger.debug("Input parameters: {}".format(inputParameters))
3500        #
3501        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3502        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3503        #     raise Exception("Incorrect value")
3504        #
3505        # if "l" in inputParameters.keys():
3506        #     inputParameters["lots"] = inputParameters.pop("l")
3507        #
3508        # if "p" in inputParameters.keys():
3509        #     inputParameters["prices"] = inputParameters.pop("p")
3510        #
3511        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3512        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3513        #     raise Exception("Incorrect value")
3514        #
3515        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3516        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3517        #
3518        # if len(lots) != len(prices):
3519        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3520        #     raise Exception("Incorrect value")
3521        #
3522        # uLogger.debug("Extracted parameters for orders:")
3523        # uLogger.debug("lots = {}".format(lots))
3524        # uLogger.debug("prices = {}".format(prices))
3525        #
3526        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3527        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3528        # uLogger.debug("Order parameters: {}".format(result))
3529        #
3530        # return result
3531
3532    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3533        """
3534        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3535
3536        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3537        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3538        """
3539        result = False
3540        msg = "Instrument not defined!"
3541
3542        if portfolio is None or not portfolio:
3543            portfolio = self.Overview(show=False)
3544
3545        if self.ticker:
3546            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3547            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3548
3549            for iType in TKS_INSTRUMENTS:
3550                for instrument in portfolio["stat"][iType]:
3551                    if instrument["ticker"] == self.ticker:
3552                        result = True
3553                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3554                        break
3555
3556        elif self.figi:
3557            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3558            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3559
3560            for iType in TKS_INSTRUMENTS:
3561                for instrument in portfolio["stat"][iType]:
3562                    if instrument["figi"] == self.figi:
3563                        result = True
3564                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3565                        break
3566
3567        else:
3568            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3569
3570        uLogger.debug(msg)
3571
3572        return result
3573
3574    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3575        """
3576        Returns instrument from the user's portfolio if it presents there.
3577        Instrument must be defined by `ticker` (highly priority) or `figi`.
3578
3579        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3580        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3581        """
3582        result = None
3583        msg = "Instrument not defined!"
3584
3585        if portfolio is None or not portfolio:
3586            portfolio = self.Overview(show=False)
3587
3588        if self.ticker:
3589            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3590            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3591
3592            for iType in TKS_INSTRUMENTS:
3593                for instrument in portfolio["stat"][iType]:
3594                    if instrument["ticker"] == self.ticker:
3595                        result = instrument
3596                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3597                        break
3598
3599        elif self.figi:
3600            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3601            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3602
3603            for iType in TKS_INSTRUMENTS:
3604                for instrument in portfolio["stat"][iType]:
3605                    if instrument["figi"] == self.figi:
3606                        result = instrument
3607                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3608                        break
3609
3610        else:
3611            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3612
3613        uLogger.debug(msg)
3614
3615        return result
3616
3617    def RequestLimits(self) -> dict:
3618        """
3619        Method for obtaining the available funds for withdrawal for current `accountId`.
3620
3621        See also:
3622        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3623        - `OverviewLimits()` method
3624
3625        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3626                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3627                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3628                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3629        """
3630        if self.accountId is None or not self.accountId:
3631            uLogger.error("Variable `accountId` must be defined for using this method!")
3632            raise Exception("Account ID required")
3633
3634        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3635
3636        self.body = str({"accountId": self.accountId})
3637        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3638        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3639
3640        if self.moreDebug:
3641            uLogger.debug("Records about available funds for withdrawal successfully received")
3642
3643        return rawLimits
3644
3645    def OverviewLimits(self, show: bool = False) -> dict:
3646        """
3647        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3648
3649        See also: `RequestLimits()`.
3650
3651        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3652        :return: dict with raw parsed data from server and some calculated statistics about it.
3653        """
3654        if self.accountId is None or not self.accountId:
3655            uLogger.error("Variable `accountId` must be defined for using this method!")
3656            raise Exception("Account ID required")
3657
3658        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3659
3660        view = {
3661            "rawLimits": rawLimits,
3662            "limits": {  # parsed data for every currency:
3663                "money": {  # this is an array of portfolio currency positions
3664                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3665                },
3666                "blocked": {  # this is an array of blocked currency
3667                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3668                },
3669                "blockedGuarantee": {  # this is locked money under collateral for futures
3670                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3671                },
3672            },
3673        }
3674
3675        # --- Prepare text table with limits in human-readable format:
3676        if show:
3677            info = [
3678                "# Withdrawal limits\n\n",
3679                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3680                "* **Account ID:** [{}]\n".format(self.accountId),
3681            ]
3682
3683            if view["limits"]["money"]:
3684                info.extend([
3685                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3686                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3687                ])
3688
3689            else:
3690                info.append("\nNo withdrawal limits\n")
3691
3692            for curr in view["limits"]["money"].keys():
3693                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3694                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3695                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3696
3697                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3698                    "[{}]".format(curr),
3699                    "{:.2f}".format(view["limits"]["money"][curr]),
3700                    "{:.2f}".format(availableMoney),
3701                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3702                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3703                )
3704
3705                if curr == "rub":
3706                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3707
3708                else:
3709                    info.append(infoStr)
3710
3711            infoText = "".join(info)
3712
3713            uLogger.info(infoText)
3714
3715            if self.withdrawalLimitsFile:
3716                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3717                    fH.write(infoText)
3718
3719                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3720
3721        return view
3722
3723    def RequestAccounts(self) -> dict:
3724        """
3725        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3726
3727        See also:
3728        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3729        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3730        - `OverviewUserInfo()` method
3731
3732        :return: dict with raw data from server that contains accounts info. Example of dict:
3733                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3734                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3735                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3736                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3737        """
3738        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3739
3740        self.body = str({})
3741        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3742        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3743
3744        if self.moreDebug:
3745            uLogger.debug("Records about available accounts successfully received")
3746
3747        return rawAccounts
3748
3749    def RequestUserInfo(self) -> dict:
3750        """
3751        Method for requesting common user's information.
3752
3753        See also:
3754        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3755        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3756        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3757        - `OverviewUserInfo()` method
3758
3759        :return: dict with raw data from server that contains user's information. Example of dict:
3760                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3761                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3762        """
3763        uLogger.debug("Requesting common user's information. Wait, please...")
3764
3765        self.body = str({})
3766        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3767        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3768
3769        if self.moreDebug:
3770            uLogger.debug("Records about current user successfully received")
3771
3772        return rawUserInfo
3773
3774    def RequestMarginStatus(self, accountId: str = None) -> dict:
3775        """
3776        Method for requesting margin calculation for defined account ID.
3777
3778        See also:
3779        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3780        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3781        - `OverviewUserInfo()` method
3782
3783        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3784        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3785                 Example of responses:
3786                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3787                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3788                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3789                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3790                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3791                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3792        """
3793        if accountId is None or not accountId:
3794            if self.accountId is None or not self.accountId:
3795                uLogger.error("Variable `accountId` must be defined for using this method!")
3796                raise Exception("Account ID required")
3797
3798            else:
3799                accountId = self.accountId  # use `self.accountId` (main ID) by default
3800
3801        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3802
3803        self.body = str({"accountId": accountId})
3804        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3805        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3806
3807        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3808            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3809            rawMargin = {}
3810
3811        else:
3812            if self.moreDebug:
3813                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3814
3815        return rawMargin
3816
3817    def RequestTariffLimits(self) -> dict:
3818        """
3819        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3820
3821        See also:
3822        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3823        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3824        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3825        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3826        - `OverviewUserInfo()` method
3827
3828        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3829                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3830                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3831        """
3832        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3833
3834        self.body = str({})
3835        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3836        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3837
3838        if self.moreDebug:
3839            uLogger.debug("Records with limits of current tariff successfully received")
3840
3841        return rawTariffLimits
3842
3843    def RequestBondCoupons(self, iJSON: dict) -> dict:
3844        """
3845        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3846        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3847        All dates are in UTC timezone.
3848
3849        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3850        Documentation:
3851        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3852        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3853
3854        See also: `ExtendBondsData()`.
3855
3856        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3857                      If raw iJSON is not data of bond then server returns an error [400] with message:
3858                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3859        :return: dictionary with bond payment calendar. Response example
3860                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3861                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3862                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3863                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3864        """
3865        if iJSON["figi"] is None or not iJSON["figi"]:
3866            uLogger.error("FIGI must be defined for using this method!")
3867            raise Exception("FIGI required")
3868
3869        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3870        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3871
3872        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3873            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3874            self.figi,
3875            startDate,
3876            endDate,
3877        ))
3878
3879        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3880        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3881        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3882
3883        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3884            uLogger.warning("Instrument type is not bond!")
3885
3886        else:
3887            if self.moreDebug:
3888                uLogger.debug("Records about bond payment calendar successfully received")
3889
3890        return calendar
3891
3892    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3893        """
3894        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3895        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3896        coupon yields, current yields and some statistics etc.
3897
3898        WARNING! This is too long operation if a lot of bonds requested from broker server.
3899
3900        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3901
3902        :param instruments: list of strings with tickers or FIGIs.
3903        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3904                     for further used by data scientists or stock analytics.
3905        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3906                 In XLSX-file and Pandas DataFrame fields mean:
3907                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3908                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3909        """
3910        if instruments is None or not instruments:
3911            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3912            raise Exception("Ticker or FIGI required")
3913
3914        if isinstance(instruments, str):
3915            instruments = [instruments]
3916
3917        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3918
3919        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3920
3921        iCount = len(uniqueInstruments)
3922        tooLong = iCount >= 20
3923        if tooLong:
3924            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3925
3926        bonds = None
3927        for i, self.figi in enumerate(uniqueInstruments):
3928            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3929
3930            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3931                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3932                rawBond = self.SearchByFIGI(requestPrice=True)
3933
3934                # Widen raw data with UTC current time (iData["actualDateTime"]):
3935                actualDate = datetime.now(tzutc())
3936                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3937
3938                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3939                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3940
3941                # Replace some values with human-readable:
3942                iData["nominalCurrency"] = iData["nominal"]["currency"]
3943                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3944                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3945                iData["aciCurrency"] = iData["aciValue"]["currency"]
3946                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3947                iData["issueSize"] = int(iData["issueSize"])
3948                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3949                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3950                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3951                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3952                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3953                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3954                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3955                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3956                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3957                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3958
3959                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3960                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3961                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3962                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3963                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3964                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3965                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3966                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3967                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3968                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3969                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3970
3971                # Widen raw data with calendar data from `rawCalendar` values:
3972                calendarData = []
3973                if "events" in iData["rawCalendar"].keys():
3974                    for item in iData["rawCalendar"]["events"]:
3975                        calendarData.append({
3976                            "couponDate": item["couponDate"],
3977                            "couponNumber": int(item["couponNumber"]),
3978                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3979                            "payCurrency": item["payOneBond"]["currency"],
3980                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3981                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3982                            "couponStartDate": item["couponStartDate"],
3983                            "couponEndDate": item["couponEndDate"],
3984                            "couponPeriod": item["couponPeriod"],
3985                        })
3986
3987                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3988                    if "maturityDate" not in iData.keys():
3989                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3990
3991                # Widen raw data with Coupon Rate.
3992                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3993                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3994                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3995                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3996
3997                # Widen raw data with Yield to Maturity (YTM) on current date.
3998                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3999                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4000                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4001                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4002                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4003                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4004
4005                iData["calendar"] = calendarData  # adds calendar at the end
4006
4007                # Remove not used data:
4008                iData.pop("uid")
4009                iData.pop("positionUid")
4010                iData.pop("currentPrice")
4011                iData.pop("rawCalendar")
4012
4013                colNames = list(iData.keys())
4014                if bonds is None:
4015                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4016
4017                else:
4018                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4019
4020            else:
4021                uLogger.warning("Instrument is not a bond!")
4022
4023            processed = round(100 * (i + 1) / iCount, 1)
4024            if tooLong and processed % 5 == 0:
4025                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4026
4027            else:
4028                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4029
4030        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4031
4032        # Saving bonds from Pandas DataFrame to XLSX sheet:
4033        if xlsx and self.bondsXLSXFile:
4034            with pd.ExcelWriter(
4035                    path=self.bondsXLSXFile,
4036                    date_format=TKS_DATE_FORMAT,
4037                    datetime_format=TKS_DATE_TIME_FORMAT,
4038                    mode="w",
4039            ) as writer:
4040                bonds.to_excel(
4041                    writer,
4042                    sheet_name="Extended bonds data",
4043                    index=True,
4044                    encoding="UTF-8",
4045                    freeze_panes=(1, 1),
4046                )  # saving as XLSX-file with freeze first row and column as headers
4047
4048            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4049
4050        return bonds
4051
4052    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4053        """
4054        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4055
4056        WARNING! This is too long operation if a lot of bonds requested from broker server.
4057
4058        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4059
4060        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4061                        extended information about bonds: main info, current prices, bond payment calendar,
4062                        coupon yields, current yields and some statistics etc.
4063                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4064        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4065                     for further used by data scientists or stock analytics.
4066        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4067        """
4068        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4069            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4070
4071        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4072
4073        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4074        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4075        calendar = None
4076        for bond in extBonds.iterrows():
4077            for item in bond[1]["calendar"]:
4078                cData = {
4079                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4080                    "couponDate": item["couponDate"],
4081                    "figi": bond[1]["figi"],
4082                    "ticker": bond[1]["ticker"],
4083                    "name": bond[1]["name"],
4084                    "couponNumber": item["couponNumber"],
4085                    "payOneBond": item["payOneBond"],
4086                    "payCurrency": item["payCurrency"],
4087                    "couponType": item["couponType"],
4088                    "couponPeriod": item["couponPeriod"],
4089                    "fixDate": item["fixDate"],
4090                    "couponStartDate": item["couponStartDate"],
4091                    "couponEndDate": item["couponEndDate"],
4092                }
4093
4094                if calendar is None:
4095                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4096
4097                else:
4098                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4099
4100        if calendar is not None:
4101            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4102
4103            # Saving calendar from Pandas DataFrame to XLSX sheet:
4104            if xlsx:
4105                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4106
4107                with pd.ExcelWriter(
4108                        path=xlsxCalendarFile,
4109                        date_format=TKS_DATE_FORMAT,
4110                        datetime_format=TKS_DATE_TIME_FORMAT,
4111                        mode="w",
4112                ) as writer:
4113                    humanReadable = calendar.copy(deep=True)
4114                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4115                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4116                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4117                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4118                    humanReadable.columns = colNames  # human-readable column names
4119
4120                    humanReadable.to_excel(
4121                        writer,
4122                        sheet_name="Bond payments calendar",
4123                        index=False,
4124                        encoding="UTF-8",
4125                        freeze_panes=(1, 2),
4126                    )  # saving as XLSX-file with freeze first row and column as headers
4127
4128                    del humanReadable  # release df in memory
4129
4130                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4131
4132        return calendar
4133
4134    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4135        """
4136        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4137        Also, creates Markdown file with calendar data, `calendar.md` by default.
4138
4139        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4140
4141        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4142                        extended information about bonds: main info, current prices, bond payment calendar,
4143                        coupon yields, current yields and some statistics etc.
4144                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4145        :param show: if `True` then also printing bonds payment calendar to the console,
4146                     otherwise save to file `calendarFile` only. `False` by default.
4147        :return: multilines text in Markdown format with bonds payment calendar as a table.
4148        """
4149        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4150            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4151
4152        infoText = "# Bond payments calendar\n\n"
4153
4154        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4155
4156        if not (calendar is None or calendar.empty):
4157            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4158
4159            info = [
4160                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4161                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4162            ]
4163
4164            newMonth = False
4165            notOneBond = calendar["figi"].nunique() > 1
4166            for i, bond in enumerate(calendar.iterrows()):
4167                if newMonth and notOneBond:
4168                    info.append(splitLine)
4169
4170                info.append(
4171                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4172                        "  √" if bond[1]["paid"] else "  —",
4173                        bond[1]["couponDate"].split("T")[0],
4174                        bond[1]["figi"],
4175                        bond[1]["ticker"],
4176                        bond[1]["couponNumber"],
4177                        "{} {}".format(
4178                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4179                            bond[1]["payCurrency"],
4180                        ),
4181                        bond[1]["couponType"],
4182                        bond[1]["couponPeriod"],
4183                        bond[1]["fixDate"].split("T")[0],
4184                    )
4185                )
4186
4187                if i < len(calendar.values) - 1:
4188                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4189                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4190                    newMonth = False if curDate.month == nextDate.month else True
4191
4192                else:
4193                    newMonth = False
4194
4195            infoText += "".join(info)
4196
4197            if show:
4198                uLogger.info("{}".format(infoText))
4199
4200            if self.calendarFile is not None:
4201                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4202                    fH.write(infoText)
4203
4204                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4205
4206        else:
4207            infoText += "No data\n"
4208
4209        return infoText
4210
4211    def OverviewAccounts(self, show: bool = False) -> dict:
4212        """
4213        Method for parsing and show simple table with all available user accounts.
4214
4215        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4216
4217        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4218        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4219                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4220                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4221                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4222                                                        "closed": "—", "access": "Full access" }, ...}}`
4223        """
4224        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4225
4226        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4227        accounts = {
4228            item["id"]: {
4229                "type": TKS_ACCOUNT_TYPES[item["type"]],
4230                "name": item["name"],
4231                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4232                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4233                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4234                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4235            } for item in rawAccounts["accounts"]
4236        }
4237
4238        # Raw and parsed data with some fields replaced in "stat" section:
4239        view = {
4240            "rawAccounts": rawAccounts,
4241            "stat": accounts,
4242        }
4243
4244        # --- Prepare simple text table with only accounts data in human-readable format:
4245        if show:
4246            info = [
4247                "# User accounts\n\n",
4248                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4249                "| Account ID   | Type                      | Status                    | Name                           |\n",
4250                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4251            ]
4252
4253            for account in view["stat"].keys():
4254                info.extend([
4255                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4256                        account,
4257                        view["stat"][account]["type"],
4258                        view["stat"][account]["status"],
4259                        view["stat"][account]["name"],
4260                    )
4261                ])
4262
4263            infoText = "".join(info)
4264
4265            uLogger.info(infoText)
4266
4267            if self.userAccountsFile:
4268                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4269                    fH.write(infoText)
4270
4271                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4272
4273        return view
4274
4275    def OverviewUserInfo(self, show: bool = False) -> dict:
4276        """
4277        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4278
4279        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4280
4281        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4282        :return: dict with raw parsed data from server and some calculated statistics about it.
4283        """
4284        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4285        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4286        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4287        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4288        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4289        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4290
4291        # This is dict with parsed common user data:
4292        userInfo = {
4293            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4294            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4295            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4296            "tariff": rawUserInfo["tariff"],
4297        }
4298
4299        # This is an array of dict with parsed margin statuses for every account IDs:
4300        margins = {}
4301        for accountId in accounts.keys():
4302            if rawMargins[accountId]:
4303                margins[accountId] = {
4304                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4305                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4306                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4307                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4308                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4309                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4310                }
4311
4312            else:
4313                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4314
4315        unary = {}  # unary-connection limits
4316        for item in rawTariffLimits["unaryLimits"]:
4317            if item["limitPerMinute"] in unary.keys():
4318                unary[item["limitPerMinute"]].extend(item["methods"])
4319
4320            else:
4321                unary[item["limitPerMinute"]] = item["methods"]
4322
4323        stream = {}  # stream-connection limits
4324        for item in rawTariffLimits["streamLimits"]:
4325            if item["limit"] in stream.keys():
4326                stream[item["limit"]].extend(item["streams"])
4327
4328            else:
4329                stream[item["limit"]] = item["streams"]
4330
4331        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4332        limits = {
4333            "unary": unary,
4334            "stream": stream,
4335        }
4336
4337        # Raw and parsed data as an output result:
4338        view = {
4339            "rawUserInfo": rawUserInfo,
4340            "rawAccounts": rawAccounts,
4341            "rawMargins": rawMargins,
4342            "rawTariffLimits": rawTariffLimits,
4343            "stat": {
4344                "userInfo": userInfo,
4345                "accounts": accounts,
4346                "margins": margins,
4347                "limits": limits,
4348            },
4349        }
4350
4351        # --- Prepare text table with user information in human-readable format:
4352        if show:
4353            info = [
4354                "# Full user information\n\n",
4355                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4356                "## Common information\n\n",
4357                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4358                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4359                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4360                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4361                "\n## User accounts\n\n",
4362            ]
4363
4364            for account in view["stat"]["accounts"].keys():
4365                info.extend([
4366                    "### ID: [{}]\n\n".format(account),
4367                    "| Parameters           | Values                                                       |\n",
4368                    "|----------------------|--------------------------------------------------------------|\n",
4369                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4370                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4371                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4372                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4373                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4374                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4375                ])
4376
4377                if margins[account]:
4378                    info.extend([
4379                        "| Margin status:       | Enabled                                                      |\n",
4380                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4381                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4382                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4383                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4384                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4385                    ])
4386
4387                else:
4388                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4389
4390            info.extend([
4391                "\n## Current user tariff limits\n",
4392                "\nSee also:\n",
4393                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4394                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4395                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4396                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4397                "\n### Unary limits\n",
4398            ])
4399
4400            if unary:
4401                for key, values in sorted(unary.items()):
4402                    info.append("\n* Max requests per minute: {}\n".format(key))
4403
4404                    for value in values:
4405                        info.append("  - {}\n".format(value))
4406
4407            else:
4408                info.append("\nNot available\n")
4409
4410            info.append("\n### Stream limits\n")
4411
4412            if stream:
4413                for key, values in sorted(stream.items()):
4414                    info.append("\n* Max stream connections: {}\n".format(key))
4415
4416                    for value in values:
4417                        info.append("  - {}\n".format(value))
4418
4419            else:
4420                info.append("\nNot available\n")
4421
4422            infoText = "".join(info)
4423
4424            uLogger.info(infoText)
4425
4426            if self.userInfoFile:
4427                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4428                    fH.write(infoText)
4429
4430                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4431
4432        return view
4433
4434
4435class Args:
4436    """
4437    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4438    """
4439    def __init__(self, **kwargs):
4440        self.__dict__.update(kwargs)
4441
4442    def __getattr__(self, item):
4443        return None
4444
4445
4446def ParseArgs():
4447    """This function get and parse command line keys."""
4448    parser = ArgumentParser()  # command-line string parser
4449
4450    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4451    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4452
4453    # --- options:
4454
4455    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4456    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4457    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4458
4459    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4460    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4461
4462    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4463    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4464
4465    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4466
4467    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4468    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4469    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4470
4471    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4472    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4473
4474    # --- commands:
4475
4476    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4477
4478    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4479    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4480    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4481    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4482    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4483    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4484    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4485    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4486
4487    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4488    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4489    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4490    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4491    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4492    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4493
4494    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4495    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4496    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4497    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4498
4499    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4500    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4501    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4502
4503    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4504    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4505    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4506    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4507    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4508    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4509    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4510
4511    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4512    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4513    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4514    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4515    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4516
4517    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4518    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4519    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4520
4521    cmdArgs = parser.parse_args()
4522    return cmdArgs
4523
4524
4525def Main(**kwargs):
4526    """
4527    Main function for work with TKSBrokerAPI in the console.
4528
4529    See examples:
4530    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4531    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4532    """
4533    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4534
4535    if args.debug_level:
4536        uLogger.level = 10  # always debug level by default
4537        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4538
4539    exitCode = 0
4540    start = datetime.now(tzutc())
4541    uLogger.debug("=-" * 50)
4542    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4543        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4544        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4545    ))
4546
4547    # trying to calculate full current version:
4548    buildVersion = __version__
4549    try:
4550        v = version("tksbrokerapi")
4551        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4552
4553    except Exception:
4554        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4555
4556    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4557    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4558
4559    try:
4560        if args.version:
4561            print("TKSBrokerAPI {}".format(buildVersion))
4562            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4563
4564        else:
4565            # Init class for trading with Tinkoff Broker:
4566            trader = TinkoffBrokerServer(
4567                token=args.token,
4568                accountId=args.account_id,
4569                useCache=not args.no_cache,
4570            )
4571
4572            # --- set some options:
4573
4574            if args.more:
4575                trader.moreDebug = True
4576                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4577
4578            if args.ticker:
4579                if args.ticker in trader.aliasesKeys:
4580                    trader.ticker = trader.aliases[args.ticker]  # Replace some tickers with its aliases
4581
4582                else:
4583                    trader.ticker = args.ticker
4584
4585            if args.figi:
4586                trader.figi = args.figi
4587
4588            if args.depth is not None:
4589                trader.depth = args.depth
4590
4591            # --- do one command:
4592
4593            if args.list:
4594                if args.output is not None:
4595                    trader.instrumentsFile = args.output
4596
4597                trader.ShowInstrumentsInfo(show=True)
4598
4599            elif args.list_xlsx:
4600                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4601
4602            elif args.bonds_xlsx is not None:
4603                if args.output is not None:
4604                    trader.bondsXLSXFile = args.output
4605
4606                if len(args.bonds_xlsx) == 0:
4607                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4608
4609                else:
4610                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4611
4612            elif args.search:
4613                if args.output is not None:
4614                    trader.searchResultsFile = args.output
4615
4616                trader.SearchInstruments(pattern=args.search[0], show=True)
4617
4618            elif args.info:
4619                if not (args.ticker or args.figi):
4620                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4621                    raise Exception("Ticker or FIGI required")
4622
4623                if args.output is not None:
4624                    trader.infoFile = args.output
4625
4626                if args.ticker:
4627                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4628
4629                else:
4630                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4631
4632            elif args.calendar is not None:
4633                if args.output is not None:
4634                    trader.calendarFile = args.output
4635
4636                if len(args.calendar) == 0:
4637                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4638
4639                else:
4640                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4641
4642                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4643
4644            elif args.price:
4645                if not (args.ticker or args.figi):
4646                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4647                    raise Exception("Ticker or FIGI required")
4648
4649                trader.GetCurrentPrices(show=True)
4650
4651            elif args.prices is not None:
4652                if args.output is not None:
4653                    trader.pricesFile = args.output
4654
4655                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4656
4657            elif args.overview:
4658                if args.output is not None:
4659                    trader.overviewFile = args.output
4660
4661                trader.Overview(show=True, details="full")
4662
4663            elif args.overview_digest:
4664                if args.output is not None:
4665                    trader.overviewDigestFile = args.output
4666
4667                trader.Overview(show=True, details="digest")
4668
4669            elif args.overview_positions:
4670                if args.output is not None:
4671                    trader.overviewPositionsFile = args.output
4672
4673                trader.Overview(show=True, details="positions")
4674
4675            elif args.overview_orders:
4676                if args.output is not None:
4677                    trader.overviewOrdersFile = args.output
4678
4679                trader.Overview(show=True, details="orders")
4680
4681            elif args.overview_analytics:
4682                if args.output is not None:
4683                    trader.overviewAnalyticsFile = args.output
4684
4685                trader.Overview(show=True, details="analytics")
4686
4687            elif args.overview_calendar:
4688                if args.output is not None:
4689                    trader.overviewAnalyticsFile = args.output
4690
4691                trader.Overview(show=True, details="calendar")
4692
4693            elif args.deals is not None:
4694                if args.output is not None:
4695                    trader.reportFile = args.output
4696
4697                if 0 <= len(args.deals) < 3:
4698                    trader.Deals(
4699                        start=args.deals[0] if len(args.deals) >= 1 else None,
4700                        end=args.deals[1] if len(args.deals) == 2 else None,
4701                        show=True,  # Always show deals report in console
4702                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4703                    )
4704
4705                else:
4706                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4707                    raise Exception("Incorrect value")
4708
4709            elif args.history is not None:
4710                if args.output is not None:
4711                    trader.historyFile = args.output
4712
4713                if 0 <= len(args.history) < 3:
4714                    dataReceived = trader.History(
4715                        start=args.history[0] if len(args.history) >= 1 else None,
4716                        end=args.history[1] if len(args.history) == 2 else None,
4717                        interval="hour" if args.interval is None or not args.interval else args.interval,
4718                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4719                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4720                        show=True,  # shows all downloaded candles in console
4721                    )
4722
4723                    if args.render_chart is not None and dataReceived is not None:
4724                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4725
4726                        trader.ShowHistoryChart(
4727                            candles=dataReceived,
4728                            interact=iChart,
4729                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4730                        )
4731
4732                else:
4733                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4734                    raise Exception("Incorrect value")
4735
4736            elif args.load_history is not None:
4737                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4738
4739                if args.render_chart is not None and histData is not None:
4740                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4741                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4742
4743                    trader.ShowHistoryChart(
4744                        candles=histData,
4745                        interact=iChart,
4746                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4747                    )
4748
4749            elif args.trade is not None:
4750                if 1 <= len(args.trade) <= 5:
4751                    trader.Trade(
4752                        operation=args.trade[0],
4753                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4754                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4755                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4756                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4757                    )
4758
4759                else:
4760                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4761
4762            elif args.buy is not None:
4763                if 0 <= len(args.buy) <= 4:
4764                    trader.Buy(
4765                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4766                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4767                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4768                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4769                    )
4770
4771                else:
4772                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4773
4774            elif args.sell is not None:
4775                if 0 <= len(args.sell) <= 4:
4776                    trader.Sell(
4777                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4778                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4779                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4780                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4781                    )
4782
4783                else:
4784                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4785
4786            elif args.order:
4787                if 4 <= len(args.order) <= 7:
4788                    trader.Order(
4789                        operation=args.order[0],
4790                        orderType=args.order[1],
4791                        lots=int(args.order[2]),
4792                        targetPrice=float(args.order[3]),
4793                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4794                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4795                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4796                    )
4797
4798                else:
4799                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4800
4801            elif args.buy_limit:
4802                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4803
4804            elif args.sell_limit:
4805                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4806
4807            elif args.buy_stop:
4808                if 2 <= len(args.buy_stop) <= 7:
4809                    trader.BuyStop(
4810                        lots=int(args.buy_stop[0]),
4811                        targetPrice=float(args.buy_stop[1]),
4812                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4813                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4814                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4815                    )
4816
4817                else:
4818                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4819
4820            elif args.sell_stop:
4821                if 2 <= len(args.sell_stop) <= 7:
4822                    trader.SellStop(
4823                        lots=int(args.sell_stop[0]),
4824                        targetPrice=float(args.sell_stop[1]),
4825                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4826                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4827                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4828                    )
4829
4830                else:
4831                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4832
4833            # elif args.buy_order_grid is not None:
4834            #     # update order grid work with api v2
4835            #     if len(args.buy_order_grid) == 2:
4836            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4837            #
4838            #         for order in orderParams:
4839            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4840            #
4841            #     else:
4842            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4843            #
4844            # elif args.sell_order_grid is not None:
4845            #     # update order grid work with api v2
4846            #     if len(args.sell_order_grid) >= 2:
4847            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4848            #
4849            #         for order in orderParams:
4850            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4851            #
4852            #     else:
4853            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4854
4855            elif args.close_order is not None:
4856                trader.CloseOrders(args.close_order)  # close only one order
4857
4858            elif args.close_orders is not None:
4859                trader.CloseOrders(args.close_orders)  # close list of orders
4860
4861            elif args.close_trade:
4862                if not (args.ticker or args.figi):
4863                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4864                    raise Exception("Ticker or FIGI required")
4865
4866                if args.ticker:
4867                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4868
4869                else:
4870                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4871
4872            elif args.close_trades is not None:
4873                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4874
4875            elif args.close_all is not None:
4876                trader.CloseAll(*args.close_all)
4877
4878            elif args.limits:
4879                if args.output is not None:
4880                    trader.withdrawalLimitsFile = args.output
4881
4882                trader.OverviewLimits(show=True)
4883
4884            elif args.user_info:
4885                if args.output is not None:
4886                    trader.userInfoFile = args.output
4887
4888                trader.OverviewUserInfo(show=True)
4889
4890            elif args.account:
4891                if args.output is not None:
4892                    trader.userAccountsFile = args.output
4893
4894                trader.OverviewAccounts(show=True)
4895
4896            else:
4897                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4898                raise Exception("There is no command to execute")
4899
4900    except Exception:
4901        trace = tb.format_exc()
4902        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4903            if e in trace:
4904                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4905                break
4906
4907        uLogger.debug(trace)
4908        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4909        exitCode = 255  # an error occurred, must be open a ticket for this issue
4910
4911    finally:
4912        finish = datetime.now(tzutc())
4913
4914        if exitCode == 0:
4915            if args.more:
4916                uLogger.debug("All operations were finished success (summary code is 0).")
4917
4918        else:
4919            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4920                os.path.abspath(uLog.defaultLogFile), exitCode,
4921            ))
4922
4923        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4924        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4925            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4926            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4927        ))
4928        uLogger.debug("=-" * 50)
4929
4930        if not kwargs:
4931            sys.exit(exitCode)
4932
4933        else:
4934            return exitCode
4935
4936
4937if __name__ == "__main__":
4938    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 — how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 253
 254        See also: `SearchByTicker()`, `SearchInstruments()`.
 255        """
 256
 257        self.figi = ""
 258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 259
 260        See also: `SearchByFIGI()`, `SearchInstruments()`.
 261        """
 262
 263        self.depth = 1
 264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 265
 266        See also: `GetCurrentPrices()`.
 267        """
 268
 269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 271
 272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 273        """
 274
 275        uLogger.debug("Broker API server: {}".format(self.server))
 276
 277        self.timeout = 15
 278        """Server operations timeout in seconds. Default: `15`.
 279
 280        See also: `SendAPIRequest()`.
 281        """
 282
 283        self.headers = {
 284            "Content-Type": "application/json",
 285            "accept": "application/json",
 286            "Authorization": "Bearer {}".format(self.token),
 287            "x-app-name": "Tim55667757.TKSBrokerAPI",
 288        }
 289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 290
 291        See also: `SendAPIRequest()`.
 292        """
 293
 294        self.body = None
 295        """Request body which send to broker server. Default: `None`.
 296
 297        See also: `SendAPIRequest()`.
 298        """
 299
 300        self.moreDebug = False
 301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 302
 303        self.historyFile = None
 304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 305
 306        See also: `History()`.
 307        """
 308
 309        self.htmlHistoryFile = "index.html"
 310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 311
 312        See also: `ShowHistoryChart()`.
 313        """
 314
 315        self.instrumentsFile = "instruments.md"
 316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 317
 318        See also: `ShowInstrumentsInfo()`.
 319        """
 320
 321        self.searchResultsFile = "search-results.md"
 322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 323
 324        See also: `SearchInstruments()`.
 325        """
 326
 327        self.pricesFile = "prices.md"
 328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 329
 330        See also: `GetListOfPrices()`.
 331        """
 332
 333        self.infoFile = "info.md"
 334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 335
 336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 337        """
 338
 339        self.bondsXLSXFile = "ext-bonds.xlsx"
 340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 342
 343        See also: `ExtendBondsData()`.
 344        """
 345
 346        self.calendarFile = "calendar.md"
 347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 348        
 349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 350
 351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 352        """
 353
 354        self.overviewFile = "overview.md"
 355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 356
 357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 358        """
 359
 360        self.overviewDigestFile = "overview-digest.md"
 361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 362
 363        See also: `Overview()` with parameter `details="digest"`.
 364        """
 365
 366        self.overviewPositionsFile = "overview-positions.md"
 367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 368
 369        See also: `Overview()` with parameter `details="positions"`.
 370        """
 371
 372        self.overviewOrdersFile = "overview-orders.md"
 373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 374
 375        See also: `Overview()` with parameter `details="orders"`.
 376        """
 377
 378        self.overviewAnalyticsFile = "overview-analytics.md"
 379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 380
 381        See also: `Overview()` with parameter `details="analytics"`.
 382        """
 383
 384        self.overviewBondsCalendarFile = "overview-calendar.md"
 385        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 386
 387        See also: `Overview()` with parameter `details="calendar"`.
 388        """
 389
 390        self.reportFile = "deals.md"
 391        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 392
 393        See also: `Deals()`.
 394        """
 395
 396        self.withdrawalLimitsFile = "limits.md"
 397        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 398
 399        See also: `OverviewLimits()` and `RequestLimits()`.
 400        """
 401
 402        self.userInfoFile = "user-info.md"
 403        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 404
 405        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 406        """
 407
 408        self.userAccountsFile = "accounts.md"
 409        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 410
 411        See also: `OverviewAccounts()`, `RequestAccounts()`.
 412        """
 413
 414        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 415        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 416
 417        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 418
 419        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 420        """
 421
 422        self.iList = None  # init iList for raw instruments data
 423        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 424        
 425        See also: `Listing()`, `DumpInstruments()`.
 426        """
 427
 428        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 429        if useCache:
 430            if os.path.exists(self.iListDumpFile):
 431                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 432                curTime = datetime.now(tzutc())
 433
 434                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 435                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 436
 437                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 438
 439                else:
 440                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 441
 442                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 443                        os.path.abspath(self.iListDumpFile),
 444                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 445                    ))
 446
 447            else:
 448                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 449                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 450
 451        else:
 452            self.iList = self.Listing()  # request new raw instruments data from broker server
 453            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 454
 455        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 456        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 457
 458        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 459        """
 460
 461    def _ParseJSON(self, rawData="{}") -> dict:
 462        """
 463        Parse JSON from response string.
 464
 465        :param rawData: this is a string with JSON-formatted text.
 466        :return: JSON (dictionary), parsed from server response string.
 467        """
 468        responseJSON = json.loads(rawData) if rawData else {}
 469
 470        if self.moreDebug:
 471            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 472
 473        return responseJSON
 474
 475    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 476        """
 477        Send GET or POST request to broker server and receive JSON object.
 478
 479        self.header: must be defining with dictionary of headers.
 480        self.body: if define then used as request body. None by default.
 481        self.timeout: global request timeout, 15 seconds by default.
 482        :param url: url with REST request.
 483        :param reqType: send "GET" or "POST" request. "GET" by default.
 484        :param retry: how many times retry after first request if an 5xx server errors occurred.
 485        :param pause: sleep time in seconds between retries.
 486        :return: response JSON (dictionary) from broker.
 487        """
 488        if reqType not in ("GET", "POST"):
 489            uLogger.error("You can define request type: 'GET' or 'POST'!")
 490            raise Exception("Incorrect value")
 491
 492        if self.moreDebug:
 493            uLogger.debug("Request parameters:")
 494            uLogger.debug("    - REST API URL: {}".format(url))
 495            uLogger.debug("    - request type: {}".format(reqType))
 496            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 497            uLogger.debug("    - body:\n{}".format(self.body))
 498
 499        # fast hack to avoid all operations with some tickers/FIGI
 500        responseJSON = {}
 501        oK = True
 502        for item in self.exclude:
 503            if item in url:
 504                if self.moreDebug:
 505                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 506
 507                oK = False
 508                break
 509
 510        if oK:
 511            counter = 0
 512            response = None
 513            errMsg = ""
 514
 515            while not response and counter <= retry:
 516                if reqType == "GET":
 517                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 518
 519                if reqType == "POST":
 520                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 521
 522                if self.moreDebug:
 523                    uLogger.debug("Response:")
 524                    uLogger.debug("    - status code: {}".format(response.status_code))
 525                    uLogger.debug("    - reason: {}".format(response.reason))
 526                    uLogger.debug("    - body length: {}".format(len(response.text)))
 527                    uLogger.debug("    - headers:\n{}".format(response.headers))
 528
 529                # Server returns some headers:
 530                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 531                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 532                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 533                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 534                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 535                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 536                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 537                    sleep(rateLimitWait)
 538
 539                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 540                if 400 <= response.status_code < 500:
 541                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 542                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 543                    counter = retry + 1
 544
 545                if 500 <= response.status_code < 600:
 546                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 547                    uLogger.debug("    - not oK, {}".format(errMsg))
 548                    counter += 1
 549
 550                    if counter <= retry:
 551                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 552                        sleep(pause)
 553
 554            responseJSON = self._ParseJSON(rawData=response.text)
 555
 556            if errMsg:
 557                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 558                uLogger.error("    - not oK, {}".format(errMsg))
 559
 560        return responseJSON
 561
 562    def _IUpdater(self, iType: str) -> tuple:
 563        """
 564        Request instrument by type from server. See available API methods for instruments:
 565        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 566        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 567        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 568        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 569        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 570
 571        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 572        :return: tuple with iType name and list of available instruments of current type for defined user token.
 573        """
 574        result = []
 575
 576        if iType in TKS_INSTRUMENTS:
 577            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 578
 579            # all instruments have the same body in API v2 requests:
 580            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 581            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 582            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 583
 584        return iType, result
 585
 586    def _IWrapper(self, kwargs):
 587        """
 588        Wrapper runs instrument's update method `_IUpdater()`.
 589        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 590        """
 591        return self._IUpdater(**kwargs)
 592
 593    def Listing(self) -> dict:
 594        """
 595        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 596
 597        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 598        """
 599        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 600        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 601
 602        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 603        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 604        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 605
 606        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 607        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 608        poolUpdater.close()
 609
 610        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 611        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 612        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 613
 614        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 615        for iType in iList.keys():
 616            for ticker in iList[iType]:
 617                iList[iType][ticker]["type"] = iType
 618
 619                if "minPriceIncrement" in iList[iType][ticker].keys():
 620                    iList[iType][ticker]["step"] = NanoToFloat(
 621                        iList[iType][ticker]["minPriceIncrement"]["units"],
 622                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 623                    )
 624
 625                else:
 626                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 627
 628        return iList
 629
 630    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 631        """
 632        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 633
 634        See also: `DumpInstruments()`, `Listing()`.
 635
 636        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 637                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 638        """
 639        if self.iListDumpFile is None or not self.iListDumpFile:
 640            uLogger.error("Output name of dump file must be defined!")
 641            raise Exception("Filename required")
 642
 643        if not self.iList or forceUpdate:
 644            self.iList = self.Listing()
 645
 646        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 647
 648        # Save as XLSX with separated sheets for every type of instruments:
 649        with pd.ExcelWriter(
 650                path=xlsxDumpFile,
 651                date_format=TKS_DATE_FORMAT,
 652                datetime_format=TKS_DATE_TIME_FORMAT,
 653                mode="w",
 654        ) as writer:
 655            for iType in TKS_INSTRUMENTS:
 656                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 657                df = df[sorted(df)]  # sorted by column names
 658                df = df.applymap(
 659                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 660                    na_action="ignore",
 661                )  # converting numbers from nano-type to float in every cell
 662                df.to_excel(
 663                    writer,
 664                    sheet_name=iType,
 665                    encoding="UTF-8",
 666                    freeze_panes=(1, 1),
 667                )  # saving as XLSX-file with freeze first row and column as headers
 668
 669        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 670
 671    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 672        """
 673        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 674        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 675
 676        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 677
 678        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 679                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 680        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 681        """
 682        if self.iListDumpFile is None or not self.iListDumpFile:
 683            uLogger.error("Output name of dump file must be defined!")
 684            raise Exception("Filename required")
 685
 686        if not self.iList or forceUpdate:
 687            self.iList = self.Listing()
 688
 689        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 690        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 691            fH.write(jsonDump)
 692
 693        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 694
 695        return jsonDump
 696
 697    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 698        """
 699        Show information about one instrument defined by json data and prints it in Markdown format.
 700
 701        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 702
 703        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 704        :param show: if `True` then also printing information about instrument and its current price.
 705        :return: multilines text in Markdown format with information about one instrument.
 706        """
 707        splitLine = "|                                                             |                                                        |\n"
 708        infoText = ""
 709
 710        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 711            info = [
 712                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 713                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 714                "| Parameters                                                  | Values                                                 |\n",
 715                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 716                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 717                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 718            ]
 719
 720            if "sector" in iJSON.keys() and iJSON["sector"]:
 721                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 722
 723            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 724                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 725                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 726            )))
 727
 728            info.extend([
 729                splitLine,
 730                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 731                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 732            ])
 733
 734            if "isin" in iJSON.keys() and iJSON["isin"]:
 735                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 736
 737            if "classCode" in iJSON.keys():
 738                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 739
 740            info.extend([
 741                splitLine,
 742                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 743                splitLine,
 744                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 745                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 746                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 747            ])
 748
 749            if iJSON["figi"]:
 750                self.figi = iJSON["figi"]
 751                iJSON = iJSON | self.RequestTradingStatus()
 752
 753                info.extend([
 754                    splitLine,
 755                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 756                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 757                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 758                ])
 759
 760            info.append(splitLine)
 761
 762            if "type" in iJSON.keys() and iJSON["type"]:
 763                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 764
 765            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 766                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 767
 768            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 769                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 772                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 773
 774            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 775                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 776
 777            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 778                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 779
 780            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 781                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 782
 783            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 784                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 785
 786            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 787                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 788
 789            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 790                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 791
 792            if "currency" in iJSON.keys():
 793                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 794
 795            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 796                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 797
 798            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 799                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 802                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 805                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 808                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 811                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 812
 813            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 814                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 815
 816            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 817                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 818
 819            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 820                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 821
 822            iExt = None
 823            if iJSON["type"] == "Bonds":
 824                info.extend([
 825                    splitLine,
 826                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 827                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 828                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 829                        iJSON["nominal"]["currency"],
 830                    )),
 831                ])
 832
 833                if "floatingCouponFlag" in iJSON.keys():
 834                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 835
 836                if "amortizationFlag" in iJSON.keys():
 837                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 838
 839                info.append(splitLine)
 840
 841                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 842                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 843
 844                if iJSON["figi"]:
 845                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 846
 847                    info.extend([
 848                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 849                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 850                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 851                    ])
 852
 853                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 854                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 855                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 856                        iJSON["aciValue"]["currency"]
 857                    )))
 858
 859            if "currentPrice" in iJSON.keys():
 860                info.append(splitLine)
 861
 862                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 863                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 864
 865                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 866                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 867                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 868                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 869                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 870
 871                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 872                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 873
 874                info.extend([
 875                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 876                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 877                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 878                    )),
 879                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 880                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 881                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 882                    )),
 883                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 884                        "{:.2f}%{}".format(
 885                            iJSON["currentPrice"]["changes"],
 886                            " ({}{:.2f} {})".format(
 887                                "+" if bondChangesDelta > 0 else "",
 888                                bondChangesDelta,
 889                                aciCurrency
 890                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 891                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 892                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 893                                currency
 894                            ),
 895                        )
 896                    ),
 897                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 898                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 899                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 900                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 902                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 903                    )),
 904                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 905                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 906                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 907                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 908                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 909                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 910                    )),
 911                ])
 912
 913            if "lot" in iJSON.keys():
 914                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 915
 916            if "step" in iJSON.keys() and iJSON["step"] != 0:
 917                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 918
 919            # Add bond payment calendar:
 920            if iJSON["type"] == "Bonds":
 921                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 922                info.extend(["\n", strCalendar])
 923
 924            infoText += "".join(info)
 925
 926            if show:
 927                uLogger.info("{}".format(infoText))
 928
 929            else:
 930                uLogger.debug("{}".format(infoText))
 931
 932            if self.infoFile is not None:
 933                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 934                    fH.write(infoText)
 935
 936                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 937
 938        return infoText
 939
 940    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 941        """
 942        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 943
 944        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 945        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 946        :return: JSON formatted data with information about instrument.
 947        """
 948        tickerJSON = {}
 949        if self.moreDebug:
 950            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 951
 952        if not self.ticker:
 953            uLogger.warning("self.ticker variable is not be empty!")
 954
 955        else:
 956            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 957                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 958                raise Exception("Instrument not allowed")
 959
 960            if not self.iList:
 961                self.iList = self.Listing()
 962
 963            if self.ticker in self.iList["Shares"].keys():
 964                tickerJSON = self.iList["Shares"][self.ticker]
 965                if self.moreDebug:
 966                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 967
 968            elif self.ticker in self.iList["Currencies"].keys():
 969                tickerJSON = self.iList["Currencies"][self.ticker]
 970                if self.moreDebug:
 971                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 972
 973            elif self.ticker in self.iList["Bonds"].keys():
 974                tickerJSON = self.iList["Bonds"][self.ticker]
 975                if self.moreDebug:
 976                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 977
 978            elif self.ticker in self.iList["Etfs"].keys():
 979                tickerJSON = self.iList["Etfs"][self.ticker]
 980                if self.moreDebug:
 981                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 982
 983            elif self.ticker in self.iList["Futures"].keys():
 984                tickerJSON = self.iList["Futures"][self.ticker]
 985                if self.moreDebug:
 986                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 987
 988        if tickerJSON:
 989            self.figi = tickerJSON["figi"]
 990
 991            if requestPrice:
 992                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 993
 994                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 995                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 996
 997                else:
 998                    tickerJSON["currentPrice"]["changes"] = 0
 999
1000            if show:
1001                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1002
1003        else:
1004            if show:
1005                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1006
1007        return tickerJSON
1008
1009    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1010        """
1011        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :return: JSON formatted data with information about instrument.
1016        """
1017        figiJSON = {}
1018        if self.moreDebug:
1019            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1020
1021        if not self.figi:
1022            uLogger.warning("self.figi variable is not be empty!")
1023
1024        else:
1025            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1026                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1027                raise Exception("Instrument not allowed")
1028
1029            if not self.iList:
1030                self.iList = self.Listing()
1031
1032            for item in self.iList["Shares"].keys():
1033                if self.figi == self.iList["Shares"][item]["figi"]:
1034                    figiJSON = self.iList["Shares"][item]
1035
1036                    if self.moreDebug:
1037                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1038
1039                    break
1040
1041            if not figiJSON:
1042                for item in self.iList["Currencies"].keys():
1043                    if self.figi == self.iList["Currencies"][item]["figi"]:
1044                        figiJSON = self.iList["Currencies"][item]
1045
1046                        if self.moreDebug:
1047                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1048
1049                        break
1050
1051            if not figiJSON:
1052                for item in self.iList["Bonds"].keys():
1053                    if self.figi == self.iList["Bonds"][item]["figi"]:
1054                        figiJSON = self.iList["Bonds"][item]
1055
1056                        if self.moreDebug:
1057                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1058
1059                        break
1060
1061            if not figiJSON:
1062                for item in self.iList["Etfs"].keys():
1063                    if self.figi == self.iList["Etfs"][item]["figi"]:
1064                        figiJSON = self.iList["Etfs"][item]
1065
1066                        if self.moreDebug:
1067                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1068
1069                        break
1070
1071            if not figiJSON:
1072                for item in self.iList["Futures"].keys():
1073                    if self.figi == self.iList["Futures"][item]["figi"]:
1074                        figiJSON = self.iList["Futures"][item]
1075
1076                        if self.moreDebug:
1077                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1078
1079                        break
1080
1081        if figiJSON:
1082            self.figi = figiJSON["figi"]
1083            self.ticker = figiJSON["ticker"]
1084
1085            if requestPrice:
1086                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1087
1088                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1089                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1090
1091                else:
1092                    figiJSON["currentPrice"]["changes"] = 0
1093
1094            if show:
1095                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1096
1097        else:
1098            if show:
1099                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1100
1101        return figiJSON
1102
1103    def GetCurrentPrices(self, show: bool = True) -> dict:
1104        """
1105        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1106        `{"buy": [{"price": 1243.8, "quantity": 193},
1107                  {"price": 1244.0, "quantity": 168},
1108                  {"price": 1244.8, "quantity": 5},
1109                  {"price": 1245.0, "quantity": 61},
1110                  {"price": 1245.4, "quantity": 60}],
1111          "sell": [{"price": 1243.6, "quantity": 8},
1112                   {"price": 1242.6, "quantity": 10},
1113                   {"price": 1242.4, "quantity": 18},
1114                   {"price": 1242.2, "quantity": 50},
1115                   {"price": 1242.0, "quantity": 113}],
1116          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1117        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1118        - sell: list of dicts with Buyers prices,
1119            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1120            - quantity: volume value by current price in lots,
1121        - limitUp: current trade session limit price, maximum,
1122        - limitDown: current trade session limit price, minimum,
1123        - lastPrice: last deal price of the instrument,
1124        - closePrice: previous trade session close price of the instrument.
1125
1126        See also: `SearchByTicker()` and `SearchByFIGI()`.
1127        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1128        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1129
1130        :param show: if `True` then print DOM to log and console.
1131        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1132                 If an error occurred then returns an empty record:
1133                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1134        """
1135        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1136
1137        if self.depth < 1:
1138            uLogger.error("Depth of Market (DOM) must be >=1!")
1139            raise Exception("Incorrect value")
1140
1141        if not (self.ticker or self.figi):
1142            uLogger.error("self.ticker or self.figi variables must be defined!")
1143            raise Exception("Ticker or FIGI required")
1144
1145        if self.ticker and not self.figi:
1146            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1147            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1148
1149        if not self.ticker and self.figi:
1150            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1151            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1152
1153        if not self.figi:
1154            uLogger.error("FIGI is not defined!")
1155            raise Exception("Ticker or FIGI required")
1156
1157        else:
1158            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1159
1160            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1161            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1162            self.body = str({"figi": self.figi, "depth": self.depth})
1163            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1164
1165            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1166                # list of dicts with sellers orders:
1167                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1168
1169                # list of dicts with buyers orders:
1170                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1171
1172                # max price of instrument at this time:
1173                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1174
1175                # min price of instrument at this time:
1176                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1177
1178                # last price of deal with instrument:
1179                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1180
1181                # last close price of instrument:
1182                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1183
1184            else:
1185                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1186                uLogger.debug("Server response: {}".format(pricesResponse))
1187
1188            if show:
1189                if prices["buy"] or prices["sell"]:
1190                    info = [
1191                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1192                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1193                            self.ticker,
1194                            self.figi,
1195                            self.depth,
1196                        ),
1197                        "-" * 60, "\n",
1198                        "             Orders of Buyers | Orders of Sellers\n",
1199                        "-" * 60, "\n",
1200                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1201                        "-" * 60, "\n",
1202                    ]
1203
1204                    if not prices["buy"]:
1205                        info.append("                              | No orders!\n")
1206                        sumBuy = 0
1207
1208                    else:
1209                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1210                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1211                        for item in maxMinSorted:
1212                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1213
1214                    if not prices["sell"]:
1215                        info.append("No orders!                    |\n")
1216                        sumSell = 0
1217
1218                    else:
1219                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1220                        for item in prices["sell"]:
1221                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1222
1223                    info.extend([
1224                        "-" * 60, "\n",
1225                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1226                        "-" * 60, "\n",
1227                    ])
1228
1229                    infoText = "".join(info)
1230
1231                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1232
1233                else:
1234                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1235
1236        return prices
1237
1238    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1239        """
1240        This method get and show information about all available broker instruments for current user account.
1241        If `instrumentsFile` string is not empty then also save information to this file.
1242
1243        :param show: if `True` then print results to console, if `False` — print only to file.
1244        :return: multi-lines string with all available broker instruments
1245        """
1246        if not self.iList:
1247            self.iList = self.Listing()
1248
1249        info = [
1250            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1251            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1252        ]
1253
1254        # add instruments count by type:
1255        for iType in self.iList.keys():
1256            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1257
1258        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1259        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1260
1261        # generating info tables with all instruments by type:
1262        for iType in self.iList.keys():
1263            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1264
1265            for instrument in self.iList[iType].keys():
1266                iName = self.iList[iType][instrument]["name"]  # instrument's name
1267                if len(iName) > 57:
1268                    iName = "{}...".format(iName[:54])  # right trim for a long string
1269
1270                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1271                    self.iList[iType][instrument]["ticker"],
1272                    iName,
1273                    self.iList[iType][instrument]["figi"],
1274                    self.iList[iType][instrument]["currency"],
1275                    self.iList[iType][instrument]["lot"],
1276                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1277                ))
1278
1279        infoText = "".join(info)
1280
1281        if show:
1282            uLogger.info(infoText)
1283
1284        if self.instrumentsFile:
1285            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1286                fH.write(infoText)
1287
1288            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1289
1290        return infoText
1291
1292    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1293        """
1294        This method search and show information about instruments by part of its ticker, FIGI or name.
1295        If `searchResultsFile` string is not empty then also save information to this file.
1296
1297        :param pattern: string with part of ticker, FIGI or instrument's name.
1298        :param show: if `True` then print results to console, if `False` — return list of result only.
1299        :return: list of dictionaries with all found instruments.
1300        """
1301        if not self.iList:
1302            self.iList = self.Listing()
1303
1304        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1305        compiledPattern = re.compile(pattern, re.IGNORECASE)
1306
1307        for iType in self.iList:
1308            for instrument in self.iList[iType].values():
1309                searchResult = compiledPattern.search(" ".join(
1310                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1311                ))
1312
1313                if searchResult:
1314                    searchResults[iType][instrument["ticker"]] = instrument
1315
1316        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1317        info = [
1318            "# Search results\n\n",
1319            "* **Search pattern:** [{}]\n".format(pattern),
1320            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1321            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1322        ]
1323        infoShort = info[:]
1324
1325        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1326        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1327        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1328
1329        if resultsLen == 0:
1330            info.append("\nNo results\n")
1331            infoShort.append("\nNo results\n")
1332            uLogger.warning("No results. Try changing your search pattern.")
1333
1334        else:
1335            for iType in searchResults:
1336                iTypeValuesCount = len(searchResults[iType].values())
1337                if iTypeValuesCount > 0:
1338                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340
1341                    for instrument in searchResults[iType].values():
1342                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1343                            instrument["type"],
1344                            instrument["ticker"],
1345                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1346                            instrument["figi"],
1347                        ))
1348
1349                    if iTypeValuesCount <= 5:
1350                        infoShort.extend(info[-iTypeValuesCount:])
1351
1352                    else:
1353                        infoShort.extend(info[-5:])
1354                        infoShort.append(skippedLine)
1355
1356        infoText = "".join(info)
1357        infoTextShort = "".join(infoShort)
1358
1359        if show:
1360            uLogger.info(infoTextShort)
1361            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1362
1363        if self.searchResultsFile:
1364            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1365                fH.write(infoText)
1366
1367            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1368
1369        return searchResults
1370
1371    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1372        """
1373        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1374
1375        :param instruments: list of strings with tickers or FIGIs.
1376        :return: list with unique instrument FIGIs only.
1377        """
1378        requestedInstruments = []
1379        for iName in instruments:
1380            if iName not in self.aliases.keys():
1381                if iName not in requestedInstruments:
1382                    requestedInstruments.append(iName)
1383
1384            else:
1385                if iName not in requestedInstruments:
1386                    if self.aliases[iName] not in requestedInstruments:
1387                        requestedInstruments.append(self.aliases[iName])
1388
1389        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1390
1391        onlyUniqueFIGIs = []
1392        for iName in requestedInstruments:
1393            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1394                continue
1395
1396            self.ticker = iName
1397            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1398
1399            if not iData:
1400                self.ticker = ""
1401                self.figi = iName
1402
1403                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1404
1405                if not iData:
1406                    self.figi = ""
1407                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1408
1409            if iData and iData["figi"] not in onlyUniqueFIGIs:
1410                onlyUniqueFIGIs.append(iData["figi"])
1411
1412        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1413
1414        return onlyUniqueFIGIs
1415
1416    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1417        """
1418        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1419
1420        See limits: https://tinkoff.github.io/investAPI/limits/
1421
1422        If `pricesFile` string is not empty then also save information to this file.
1423
1424        :param instruments: list of strings with tickers or FIGIs.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1427                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1428        """
1429        if instruments is None or not instruments:
1430            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1431            raise Exception("Ticker or FIGI required")
1432
1433        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1434
1435        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1436
1437        iList = []  # trying to get info and current prices about all unique instruments:
1438        for self.figi in onlyUniqueFIGIs:
1439            iData = self.SearchByFIGI(requestPrice=True)
1440            iList.append(iData)
1441
1442        self.ShowListOfPrices(iList, show)
1443
1444        return iList
1445
1446    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1447        """
1448        Show table contains current prices of given instruments.
1449
1450        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1451                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1452        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1453        :return: multilines text in Markdown format as a table contains current prices.
1454        """
1455        infoText = ""
1456
1457        if show or self.pricesFile:
1458            info = [
1459                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1460                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1461                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1462            ]
1463
1464            for item in iList:
1465                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1466                    item["ticker"],
1467                    item["figi"],
1468                    item["type"],
1469                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1470                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1471                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1472                    "{} / {}".format(
1473                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1474                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1475                    ),
1476                    "{} / {}".format(
1477                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1478                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1479                    ),
1480                    item["currency"],
1481                ))
1482
1483            infoText = "".join(info)
1484
1485            if show:
1486                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1487
1488            if self.pricesFile:
1489                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1490                    fH.write(infoText)
1491
1492                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1493
1494        return infoText
1495
1496    def RequestTradingStatus(self) -> dict:
1497        """
1498        Requesting trading status for the instrument defined by `figi` variable.
1499
1500        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1501
1502        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1503
1504        :return: dictionary with trading status attributes. Response example:
1505                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1506                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1507        """
1508        if self.figi is None or not self.figi:
1509            uLogger.error("Variable `figi` must be defined for using this method!")
1510            raise Exception("FIGI required")
1511
1512        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1513
1514        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1515        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1516        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1517
1518        if self.moreDebug:
1519            uLogger.debug("Records about current trading status successfully received")
1520
1521        return tradingStatus
1522
1523    def RequestPortfolio(self) -> dict:
1524        """
1525        Requesting actual user's portfolio for current `accountId`.
1526
1527        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1528
1529        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1530
1531        :return: dictionary with user's portfolio.
1532        """
1533        if self.accountId is None or not self.accountId:
1534            uLogger.error("Variable `accountId` must be defined for using this method!")
1535            raise Exception("Account ID required")
1536
1537        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1538
1539        self.body = str({"accountId": self.accountId})
1540        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1541        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1542
1543        if self.moreDebug:
1544            uLogger.debug("Records about user's portfolio successfully received")
1545
1546        return rawPortfolio
1547
1548    def RequestPositions(self) -> dict:
1549        """
1550        Requesting open positions by currencies and instruments for current `accountId`.
1551
1552        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1553
1554        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1555
1556        :return: dictionary with open positions by instruments.
1557        """
1558        if self.accountId is None or not self.accountId:
1559            uLogger.error("Variable `accountId` must be defined for using this method!")
1560            raise Exception("Account ID required")
1561
1562        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1563
1564        self.body = str({"accountId": self.accountId})
1565        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1566        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1567
1568        if self.moreDebug:
1569            uLogger.debug("Records about current open positions successfully received")
1570
1571        return rawPositions
1572
1573    def RequestPendingOrders(self) -> list:
1574        """
1575        Requesting current actual pending orders for current `accountId`.
1576
1577        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1578
1579        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1580
1581        :return: list of dictionaries with pending orders.
1582        """
1583        if self.accountId is None or not self.accountId:
1584            uLogger.error("Variable `accountId` must be defined for using this method!")
1585            raise Exception("Account ID required")
1586
1587        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1588
1589        self.body = str({"accountId": self.accountId})
1590        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1591        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1592
1593        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1594
1595        return rawOrders
1596
1597    def RequestStopOrders(self) -> list:
1598        """
1599        Requesting current actual stop orders for current `accountId`.
1600
1601        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1602
1603        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1604
1605        :return: list of dictionaries with stop orders.
1606        """
1607        if self.accountId is None or not self.accountId:
1608            uLogger.error("Variable `accountId` must be defined for using this method!")
1609            raise Exception("Account ID required")
1610
1611        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1612
1613        self.body = str({"accountId": self.accountId})
1614        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1615        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1616
1617        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1618
1619        return rawStopOrders
1620
1621    def Overview(self, show: bool = False, details: str = "full") -> dict:
1622        """
1623        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1624        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1625        and `overviewBondsCalendarFile` are defined then also save information to file.
1626
1627        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1628        many requests about the state of the portfolio, and then, based on the received data, a large number
1629        of calculation and statistics are collected.
1630
1631        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1632        :param details: how detailed should the information be?
1633        - `full` — shows full available information about portfolio status (by default),
1634        - `positions` — shows only open positions,
1635        - `orders` — shows only sections of open limits and stop orders.
1636        - `digest` — show a short digest of the portfolio status,
1637        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1638        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1639        :return: dictionary with client's raw portfolio and some statistics.
1640        """
1641        if self.accountId is None or not self.accountId:
1642            uLogger.error("Variable `accountId` must be defined for using this method!")
1643            raise Exception("Account ID required")
1644
1645        view = {
1646            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1647                "headers": {},  # list of dictionaries, response headers without "positions" section
1648                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1649                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1650                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1651                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1652                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1653                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1654                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1655                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1656                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1657            },
1658            "stat": {  # --- some statistics calculated using "raw" sections:
1659                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1660                "availableRUB": 0.,  # available rubles (without other currencies)
1661                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1662                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1663                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1664                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1665                "sharesCostRUB": 0.,  # costs of all shares in RUB
1666                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1667                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1668                "futuresCostRUB": 0.,  # costs of all futures in RUB
1669                "Currencies": [],  # list of dictionaries of all currencies statistics
1670                "Shares": [],  # list of dictionaries of all shares statistics
1671                "Bonds": [],  # list of dictionaries of all bonds statistics
1672                "Etfs": [],  # list of dictionaries of all etfs statistics
1673                "Futures": [],  # list of dictionaries of all futures statistics
1674                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1675                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1676                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1677                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1678                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1679            },
1680            "analytics": {  # --- some analytics of portfolio:
1681                "distrByAssets": {},  # portfolio distribution by assets
1682                "distrByCompanies": {},  # portfolio distribution by companies
1683                "distrBySectors": {},  # portfolio distribution by sectors
1684                "distrByCurrencies": {},  # portfolio distribution by currencies
1685                "distrByCountries": {},  # portfolio distribution by countries
1686                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1687            }
1688        }
1689
1690        details = details.lower()
1691        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1692        if details not in availableDetails:
1693            details = "full"
1694            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1695
1696        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1697
1698        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1699        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1700        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1701        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1702
1703        # save response headers without "positions" section:
1704        for key in portfolioResponse.keys():
1705            if key != "positions":
1706                view["raw"]["headers"][key] = portfolioResponse[key]
1707
1708            else:
1709                continue
1710
1711        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1712        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1713        for item in portfolioResponse["positions"]:
1714            if item["instrumentType"] == "currency":
1715                self.figi = item["figi"]
1716                curr = self.SearchByFIGI(requestPrice=False)
1717
1718                # current price of currency in RUB:
1719                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1720                    "name": curr["name"],
1721                    "currentPrice": NanoToFloat(
1722                        item["currentPrice"]["units"],
1723                        item["currentPrice"]["nano"]
1724                    ),
1725                }
1726
1727                view["raw"]["Currencies"].append(item)
1728
1729            elif item["instrumentType"] == "share":
1730                view["raw"]["Shares"].append(item)
1731
1732            elif item["instrumentType"] == "bond":
1733                view["raw"]["Bonds"].append(item)
1734
1735            elif item["instrumentType"] == "etf":
1736                view["raw"]["Etfs"].append(item)
1737
1738            elif item["instrumentType"] == "futures":
1739                view["raw"]["Futures"].append(item)
1740
1741            else:
1742                continue
1743
1744        # how many volume of currencies (by ISO currency name) are blocked:
1745        for item in view["raw"]["positions"]["blocked"]:
1746            blocked = NanoToFloat(item["units"], item["nano"])
1747            if blocked > 0:
1748                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1749
1750        # how many volume of instruments (by FIGI) are blocked:
1751        for item in view["raw"]["positions"]["securities"]:
1752            blocked = int(item["blocked"])
1753            if blocked > 0:
1754                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1755
1756        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1757
1758        if "rub" in allBlocked.keys():
1759            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1760
1761        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1762        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1763        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1764        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1765        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1766        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1767        view["stat"]["portfolioCostRUB"] = sum([
1768            view["stat"]["allCurrenciesCostRUB"],
1769            view["stat"]["sharesCostRUB"],
1770            view["stat"]["bondsCostRUB"],
1771            view["stat"]["etfsCostRUB"],
1772            view["stat"]["futuresCostRUB"],
1773        ])
1774
1775        # --- calculating some portfolio statistics:
1776        byComp = {}  # distribution by companies
1777        bySect = {}  # distribution by sectors
1778        byCurr = {}  # distribution by currencies (include RUB)
1779        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1780        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1781
1782        for item in portfolioResponse["positions"]:
1783            self.figi = item["figi"]
1784            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1785
1786            if instrument:
1787                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1788                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1789
1790                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1791                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1792
1793                else:
1794                    blocked = 0
1795
1796                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1797                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1798                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1799                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1800                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1801                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1802                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1803                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1804                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1805                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1806                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1807                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1808
1809                statData = {
1810                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1811                    "ticker": instrument["ticker"],  # ticker by FIGI
1812                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1813                    "volume": volume,  # available volume of instrument
1814                    "lots": lots,  # volume in lots of instrument
1815                    "direction": direction,  # direction of an instrument's position: short or long
1816                    "blocked": blocked,  # blocked volume of currency or instrument
1817                    "currentPrice": curPrice,  # current instrument's price in basic asset
1818                    "average": average,  # current average position price
1819                    "cost": cost,  # current cost of all volume of instrument in basic asset
1820                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1821                    "costRUB": costRUB,  # cost of instrument in ruble
1822                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1823                    "profit": profit,  # expected profit at current moment
1824                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1825                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1826                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1827                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1828                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1829                    "step": instrument["step"],  # minimum price increment
1830                }
1831
1832                # adding distribution by unique countries:
1833                if statData["country"] not in byCountry.keys():
1834                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1835
1836                else:
1837                    byCountry[statData["country"]]["cost"] += costRUB
1838                    byCountry[statData["country"]]["percent"] += percentCostRUB
1839
1840                if item["instrumentType"] != "currency":
1841                    # adding distribution by unique companies:
1842                    if statData["name"]:
1843                        if statData["name"] not in byComp.keys():
1844                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1845
1846                        else:
1847                            byComp[statData["name"]]["cost"] += costRUB
1848                            byComp[statData["name"]]["percent"] += percentCostRUB
1849
1850                    # adding distribution by unique sectors:
1851                    if statData["sector"] not in bySect.keys():
1852                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1853
1854                    else:
1855                        bySect[statData["sector"]]["cost"] += costRUB
1856                        bySect[statData["sector"]]["percent"] += percentCostRUB
1857
1858                # adding distribution by unique currencies:
1859                if currency not in byCurr.keys():
1860                    byCurr[currency] = {
1861                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1862                        "cost": costRUB,
1863                        "percent": percentCostRUB
1864                    }
1865
1866                else:
1867                    byCurr[currency]["cost"] += costRUB
1868                    byCurr[currency]["percent"] += percentCostRUB
1869
1870                # saving statistics for every instrument:
1871                if item["instrumentType"] == "currency":
1872                    view["stat"]["Currencies"].append(statData)
1873
1874                    # update dict with free funds for trading (total - blocked) by currencies
1875                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1876                    view["stat"]["funds"][currency] = {
1877                        "total": volume,
1878                        "totalCostRUB": costRUB,  # total volume cost in rubles
1879                        "free": volume - blocked,
1880                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1881                    }
1882
1883                elif item["instrumentType"] == "share":
1884                    view["stat"]["Shares"].append(statData)
1885
1886                elif item["instrumentType"] == "bond":
1887                    view["stat"]["Bonds"].append(statData)
1888
1889                elif item["instrumentType"] == "etf":
1890                    view["stat"]["Etfs"].append(statData)
1891
1892                elif item["instrumentType"] == "Futures":
1893                    view["stat"]["Futures"].append(statData)
1894
1895                else:
1896                    continue
1897
1898        # total changes in Russian Ruble:
1899        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1900        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1901        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1902        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1903        view["stat"]["funds"]["rub"] = {
1904            "total": view["stat"]["availableRUB"],
1905            "totalCostRUB": view["stat"]["availableRUB"],
1906            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1908        }
1909
1910        # --- pending orders sector data:
1911        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1912        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1913
1914        for item in view["raw"]["orders"]:
1915            self.figi = item["figi"]
1916
1917            if item["figi"] not in uniquePendingOrdersFIGIs:
1918                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1919
1920                uniquePendingOrdersFIGIs.append(item["figi"])
1921                uniquePendingOrders[item["figi"]] = instrument
1922
1923            else:
1924                instrument = uniquePendingOrders[item["figi"]]
1925
1926            if instrument:
1927                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1928                orderType = TKS_ORDER_TYPES[item["orderType"]]
1929                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1930                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1931
1932                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1933                if item["direction"] == "ORDER_DIRECTION_BUY":
1934                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1935
1936                else:
1937                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1938
1939                # requested price for order execution:
1940                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1941
1942                # necessary changes in percent to reach target from current price:
1943                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1944
1945                view["stat"]["orders"].append({
1946                    "orderID": item["orderId"],  # orderId number parameter of current order
1947                    "figi": item["figi"],  # FIGI identification
1948                    "ticker": instrument["ticker"],  # ticker name by FIGI
1949                    "lotsRequested": item["lotsRequested"],  # requested lots value
1950                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for order execution in base currency
1953                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1954                    "percentChanges": changes,  # changes in percent to target from current price
1955                    "currency": item["currency"],  # instrument's currency name
1956                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1957                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1958                    "status": orderState,  # order status from TKS_ORDER_STATES
1959                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1960                })
1961
1962        # --- stop orders sector data:
1963        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1964        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["stopOrders"]:
1967            self.figi = item["figi"]
1968
1969            if item["figi"] not in uniqueStopOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniqueStopOrdersFIGIs.append(item["figi"])
1973                uniqueStopOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniqueStopOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1981                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1982
1983                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1984                if "expirationTime" in item.keys():
1985                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1986                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1987
1988                else:
1989                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1990                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1991
1992                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1993                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1994                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1995
1996                else:
1997                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1998
1999                # requested price when stop-order executed:
2000                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2001
2002                # price for limit-order, set up when stop-order executed:
2003                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2004
2005                # necessary changes in percent to reach target from current price:
2006                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2007
2008                view["stat"]["stopOrders"].append({
2009                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2010                    "figi": item["figi"],  # FIGI identification
2011                    "ticker": instrument["ticker"],  # ticker name by FIGI
2012                    "lotsRequested": item["lotsRequested"],  # requested lots value
2013                    "currentPrice": lastPrice,  # current instrument's price for defined action
2014                    "targetPrice": target,  # requested price for stop-order execution in base currency
2015                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2016                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2017                    "percentChanges": changes,  # changes in percent to target from current price
2018                    "currency": item["currency"],  # instrument's currency name
2019                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2020                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2021                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2022                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2023                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2024                })
2025
2026        # --- calculating data for analytics section:
2027        # portfolio distribution by assets:
2028        view["analytics"]["distrByAssets"] = {
2029            "Ruble": {
2030                "uniques": 1,
2031                "cost": view["stat"]["availableRUB"],
2032                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Currencies": {
2035                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2036                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2037                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Shares": {
2040                "uniques": len(view["stat"]["Shares"]),
2041                "cost": view["stat"]["sharesCostRUB"],
2042                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Bonds": {
2045                "uniques": len(view["stat"]["Bonds"]),
2046                "cost": view["stat"]["bondsCostRUB"],
2047                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Etfs": {
2050                "uniques": len(view["stat"]["Etfs"]),
2051                "cost": view["stat"]["etfsCostRUB"],
2052                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Futures": {
2055                "uniques": len(view["stat"]["Futures"]),
2056                "cost": view["stat"]["futuresCostRUB"],
2057                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059        }
2060
2061        # portfolio distribution by companies:
2062        view["analytics"]["distrByCompanies"]["All money cash"] = {
2063            "ticker": "",
2064            "cost": view["stat"]["allCurrenciesCostRUB"],
2065            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2066        }
2067        view["analytics"]["distrByCompanies"].update(byComp)
2068
2069        # portfolio distribution by sectors:
2070        view["analytics"]["distrBySectors"]["All money cash"] = {
2071            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2072            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2073        }
2074        view["analytics"]["distrBySectors"].update(bySect)
2075
2076        # portfolio distribution by currencies:
2077        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2078            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2079
2080            if self.moreDebug:
2081                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2082
2083        view["analytics"]["distrByCurrencies"].update(byCurr)
2084        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2085        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2086
2087        # portfolio distribution by countries:
2088        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2089            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2090
2091            if self.moreDebug:
2092                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2093
2094        view["analytics"]["distrByCountries"].update(byCountry)
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2096        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2097
2098        # --- Prepare text statistics overview in human-readable:
2099        if show:
2100            # Whatever the value `details`, header not changes:
2101            info = [
2102                "# Client's portfolio\n\n",
2103                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2104                "* **Account ID:** [{}]\n".format(self.accountId),
2105            ]
2106
2107            if details in ["full", "positions", "digest"]:
2108                info.extend([
2109                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2110                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2111                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2112                        view["stat"]["totalChangesRUB"],
2113                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2114                        view["stat"]["totalChangesPercentRUB"],
2115                    ),
2116                ])
2117
2118            if details in ["full", "positions"]:
2119                info.extend([
2120                    "## Open positions\n\n",
2121                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2122                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2123                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2124                        "{:.2f} ({:.2f}) rub".format(
2125                            view["stat"]["availableRUB"],
2126                            view["stat"]["blockedRUB"],
2127                        )
2128                    )
2129                ])
2130
2131                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2132                    return [
2133                        "|                             |                                 |          |              |              |                     |                              |\n",
2134                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2135                            noTradeStr if noTradeStr else typeStr,
2136                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2137                        ),
2138                    ]
2139
2140                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2141                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2142                        "{} [{}]".format(data["ticker"], data["figi"]),
2143                        "{:.2f} ({:.2f}) {}".format(
2144                            data["volume"],
2145                            data["blocked"],
2146                            data["currency"],
2147                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2148                            data["volume"],
2149                            data["blocked"],
2150                        ),
2151                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2152                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2153                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2154                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2155                        "{}{:.2f} {} ({}{:.2f}%)".format(
2156                            "+" if data["profit"] > 0 else "",
2157                            data["profit"], data["baseCurrencyName"],
2158                            "+" if data["percentProfit"] > 0 else "",
2159                            data["percentProfit"],
2160                        ),
2161                    )
2162
2163                # --- Show currencies section:
2164                if view["stat"]["Currencies"]:
2165                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2166                    for item in view["stat"]["Currencies"]:
2167                        info.append(_InfoStr(item, showCurrencyName=True))
2168
2169                else:
2170                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2171
2172                # --- Show shares section:
2173                if view["stat"]["Shares"]:
2174                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2175
2176                    for item in view["stat"]["Shares"]:
2177                        info.append(_InfoStr(item))
2178
2179                else:
2180                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2181
2182                # --- Show bonds section:
2183                if view["stat"]["Bonds"]:
2184                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2185
2186                    for item in view["stat"]["Bonds"]:
2187                        info.append(_InfoStr(item))
2188
2189                else:
2190                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2191
2192                # --- Show etfs section:
2193                if view["stat"]["Etfs"]:
2194                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2195
2196                    for item in view["stat"]["Etfs"]:
2197                        info.append(_InfoStr(item))
2198
2199                else:
2200                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2201
2202                # --- Show futures section:
2203                if view["stat"]["Futures"]:
2204                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2205
2206                    for item in view["stat"]["Futures"]:
2207                        info.append(_InfoStr(item))
2208
2209                else:
2210                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2211
2212            if details in ["full", "orders"]:
2213                # --- Show pending orders section:
2214                if view["stat"]["orders"]:
2215                    info.extend([
2216                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2217                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2218                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2219                    ])
2220
2221                    for item in view["stat"]["orders"]:
2222                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2223                            "{} [{}]".format(item["ticker"], item["figi"]),
2224                            item["orderID"],
2225                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2226                            "{} {} ({}{:.2f}%)".format(
2227                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2228                                item["baseCurrencyName"],
2229                                "+" if item["percentChanges"] > 0 else "",
2230                                float(item["percentChanges"]),
2231                            ),
2232                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2233                            item["action"],
2234                            item["type"],
2235                            item["date"],
2236                        ))
2237
2238                else:
2239                    info.append("\n## Total pending limit-orders: 0\n")
2240
2241                # --- Show stop orders section:
2242                if view["stat"]["stopOrders"]:
2243                    info.extend([
2244                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2245                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2246                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2247                    ])
2248
2249                    for item in view["stat"]["stopOrders"]:
2250                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2251                            "{} [{}]".format(item["ticker"], item["figi"]),
2252                            item["orderID"],
2253                            item["lotsRequested"],
2254                            "{} {} ({}{:.2f}%)".format(
2255                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2256                                item["baseCurrencyName"],
2257                                "+" if item["percentChanges"] > 0 else "",
2258                                float(item["percentChanges"]),
2259                            ),
2260                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2261                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2262                            item["action"],
2263                            item["type"],
2264                            item["expType"],
2265                            item["createDate"],
2266                            item["expDate"],
2267                        ))
2268
2269                else:
2270                    info.append("\n## Total stop-orders: 0\n")
2271
2272            if details in ["full", "analytics"]:
2273                # -- Show analytics section:
2274                if view["stat"]["portfolioCostRUB"] > 0:
2275                    info.extend([
2276                        "\n# Analytics\n"
2277                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2278                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2279                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2280                            view["stat"]["totalChangesRUB"],
2281                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2282                            view["stat"]["totalChangesPercentRUB"],
2283                        ),
2284                        "\n## Portfolio distribution by assets\n"
2285                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2286                        "|------------------------------------|---------|---------|--------------------|\n",
2287                    ])
2288
2289                    for key in view["analytics"]["distrByAssets"].keys():
2290                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2291                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2292                                key,
2293                                view["analytics"]["distrByAssets"][key]["uniques"],
2294                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2295                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2296                            ))
2297
2298                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2299
2300                    info.extend([
2301                        "\n## Portfolio distribution by companies\n"
2302                        "\n| Company                                      | Percent | Current cost       |\n",
2303                        aSepLine,
2304                    ])
2305
2306                    for company in view["analytics"]["distrByCompanies"].keys():
2307                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2308                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2309                                "{}{}".format(
2310                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2311                                    company,
2312                                ),
2313                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2314                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2315                            ))
2316
2317                    info.extend([
2318                        "\n## Portfolio distribution by sectors\n"
2319                        "\n| Sector                                       | Percent | Current cost       |\n",
2320                        aSepLine,
2321                    ])
2322
2323                    for sector in view["analytics"]["distrBySectors"].keys():
2324                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2325                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2326                                sector,
2327                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2328                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2329                            ))
2330
2331                    info.extend([
2332                        "\n## Portfolio distribution by currencies\n"
2333                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2334                        aSepLine,
2335                    ])
2336
2337                    for curr in view["analytics"]["distrByCurrencies"].keys():
2338                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2339                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2340                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2341                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2342                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2343                            ))
2344
2345                    info.extend([
2346                        "\n## Portfolio distribution by countries\n"
2347                        "\n| Assets by country                            | Percent | Current cost       |\n",
2348                        aSepLine,
2349                    ])
2350
2351                    for country in view["analytics"]["distrByCountries"].keys():
2352                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2353                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2354                                country,
2355                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2356                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2357                            ))
2358
2359            if details in ["full", "calendar"]:
2360                # -- Show bonds payment calendar section:
2361                if view["stat"]["Bonds"]:
2362                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2363                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2364                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2365
2366                else:
2367                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2368
2369            infoText = "".join(info)
2370
2371            uLogger.info(infoText)
2372
2373            if details == "full" and self.overviewFile:
2374                filename = self.overviewFile
2375
2376            elif details == "digest" and self.overviewDigestFile:
2377                filename = self.overviewDigestFile
2378
2379            elif details == "positions" and self.overviewPositionsFile:
2380                filename = self.overviewPositionsFile
2381
2382            elif details == "orders" and self.overviewOrdersFile:
2383                filename = self.overviewOrdersFile
2384
2385            elif details == "analytics" and self.overviewAnalyticsFile:
2386                filename = self.overviewAnalyticsFile
2387
2388            elif details == "calendar" and self.overviewBondsCalendarFile:
2389                filename = self.overviewBondsCalendarFile
2390
2391            else:
2392                filename = ""
2393
2394            if filename:
2395                with open(filename, "w", encoding="UTF-8") as fH:
2396                    fH.write(infoText)
2397
2398                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2399
2400        return view
2401
2402    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2403        """
2404        Returns history operations between two given dates for current `accountId`.
2405        If `reportFile` string is not empty then also save human-readable report.
2406        Shows some statistical data of closed positions.
2407
2408        :param start: see docstring in `GetDatesAsString()` method
2409        :param end: see docstring in `GetDatesAsString()` method
2410        :param show: if `True` then also prints all records to the console.
2411        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2412        :return: original list of dictionaries with history of deals records from API ("operations" key):
2413                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2414                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2415        """
2416        if self.accountId is None or not self.accountId:
2417            uLogger.error("Variable `accountId` must be defined for using this method!")
2418            raise Exception("Account ID required")
2419
2420        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2421
2422        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2423
2424        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2425        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2426        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2427        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2428        customStat = {}  # custom statistics in additional to responseJSON
2429
2430        # --- output report in human-readable format:
2431        if show or self.reportFile:
2432            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2433            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2434            nextDay = ""
2435
2436            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2437
2438            if len(ops) > 0:
2439                customStat = {
2440                    "opsCount": 0,  # total operations count
2441                    "buyCount": 0,  # buy operations
2442                    "sellCount": 0,  # sell operations
2443                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2444                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2445                    "payIn": {"rub": 0.},  # Deposit brokerage account
2446                    "payOut": {"rub": 0.},  # Withdrawals
2447                    "divs": {"rub": 0.},  # Dividends income
2448                    "coupons": {"rub": 0.},  # Coupon's income
2449                    "brokerCom": {"rub": 0.},  # Service commissions
2450                    "serviceCom": {"rub": 0.},  # Service commissions
2451                    "marginCom": {"rub": 0.},  # Margin commissions
2452                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2453                }
2454
2455                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2456                for item in ops:
2457                    if item["state"] == "OPERATION_STATE_EXECUTED":
2458                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2459
2460                        # count buy operations:
2461                        if "_BUY" in item["operationType"]:
2462                            customStat["buyCount"] += 1
2463
2464                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2465                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2469
2470                        # count sell operations:
2471                        elif "_SELL" in item["operationType"]:
2472                            customStat["sellCount"] += 1
2473
2474                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2475                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2476
2477                            else:
2478                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2479
2480                        # count incoming operations:
2481                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2482                            if item["payment"]["currency"] in customStat["payIn"].keys():
2483                                customStat["payIn"][item["payment"]["currency"]] += payment
2484
2485                            else:
2486                                customStat["payIn"][item["payment"]["currency"]] = payment
2487
2488                        # count withdrawals operations:
2489                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2490                            if item["payment"]["currency"] in customStat["payOut"].keys():
2491                                customStat["payOut"][item["payment"]["currency"]] += payment
2492
2493                            else:
2494                                customStat["payOut"][item["payment"]["currency"]] = payment
2495
2496                        # count dividends income:
2497                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2498                            if item["payment"]["currency"] in customStat["divs"].keys():
2499                                customStat["divs"][item["payment"]["currency"]] += payment
2500
2501                            else:
2502                                customStat["divs"][item["payment"]["currency"]] = payment
2503
2504                        # count coupon's income:
2505                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2506                            if item["payment"]["currency"] in customStat["coupons"].keys():
2507                                customStat["coupons"][item["payment"]["currency"]] += payment
2508
2509                            else:
2510                                customStat["coupons"][item["payment"]["currency"]] = payment
2511
2512                        # count broker commissions:
2513                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2514                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2515                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2519
2520                        # count service commissions:
2521                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2522                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2523                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2527
2528                        # count margin commissions:
2529                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2530                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2531                                customStat["marginCom"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["marginCom"][item["payment"]["currency"]] = payment
2535
2536                        # count withholding taxes:
2537                        elif "_TAX" in item["operationType"]:
2538                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2539                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2543
2544                        else:
2545                            continue
2546
2547                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2548
2549                # --- view "Actions" lines:
2550                info.extend([
2551                    "| Report sections            |                               |                              |                      |                        |\n",
2552                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2553                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2554                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2555                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2556                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2557                    ),
2558                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2559                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2560                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2561                    ),
2562                ])
2563
2564                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2565                for key in opsKeys:
2566                    if key == "rub":
2567                        continue
2568
2569                    info.extend([
2570                        "|                            |                               | {:<28} |                      |                        |\n".format(
2571                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2572                        ),
2573                        "|                            |                               | {:<28} |                      |                        |\n".format(
2574                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2575                        ),
2576                    ])
2577
2578                info.append(splitLine1)
2579
2580                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2581                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2582                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2583                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2584                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2586                    )
2587
2588                # --- view "Payments" lines:
2589                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2590                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2591
2592                for key in paymentsKeys:
2593                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2594
2595                info.append(splitLine1)
2596
2597                # --- view "Commissions and taxes" lines:
2598                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2599                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2600
2601                for key in comKeys:
2602                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2603
2604                info.append(splitLine1)
2605
2606                info.extend([
2607                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2608                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2609                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2610                ])
2611
2612            else:
2613                info.append("Broker returned no operations during this period\n")
2614
2615            # --- view "Operations" section:
2616            for item in ops:
2617                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2618                    continue
2619
2620                else:
2621                    self.figi = item["figi"] if item["figi"] else ""
2622                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2623                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2624
2625                    # group of deals during one day:
2626                    if nextDay and item["date"].split("T")[0] != nextDay:
2627                        info.append(splitLine2)
2628                        nextDay = ""
2629
2630                    else:
2631                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2632
2633                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2634                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2635                        self.figi if self.figi else "—",
2636                        instrument["ticker"] if instrument else "—",
2637                        instrument["type"] if instrument else "—",
2638                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2639                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2640                        TKS_OPERATION_STATES[item["state"]],
2641                        TKS_OPERATION_TYPES[item["operationType"]],
2642                    ))
2643
2644            infoText = "".join(info)
2645
2646            if show:
2647                if self.moreDebug:
2648                    uLogger.debug("Records about history of a client's operations successfully received")
2649
2650                uLogger.info(infoText)
2651
2652            if self.reportFile:
2653                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2654                    fH.write(infoText)
2655
2656                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2657
2658        return ops, customStat
2659
2660    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2661        """
2662        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2663
2664        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2665        Warning! Broker server used ISO UTC time by default.
2666
2667        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2668        Also, `historyFile` used to update history with `onlyMissing` parameter.
2669
2670        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2671
2672        :param start: see docstring in `GetDatesAsString()` method.
2673        :param end: see docstring in `GetDatesAsString()` method.
2674        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2675                         `"hour"`, `"day"`. Default: `"hour"`.
2676        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2677                            False by default. Warning! History appends only from last candle to current time
2678                            with always update last candle!
2679        :param csvSep: separator if csv-file is used, `,` by default.
2680        :param show: if `True` then also prints Pandas DataFrame to the console.
2681        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2682                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2683        """
2684        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2685        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2686        history = None  # empty pandas object for history
2687
2688        if interval not in TKS_CANDLE_INTERVALS.keys():
2689            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2690            raise Exception("Incorrect value")
2691
2692        if not (self.ticker or self.figi):
2693            uLogger.error("Ticker or FIGI must be defined!")
2694            raise Exception("Ticker or FIGI required")
2695
2696        if self.ticker and not self.figi:
2697            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2698            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2699
2700        if self.figi and not self.ticker:
2701            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2702            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2703
2704        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2705        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2706        if interval.lower() != "day":
2707            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2708
2709        delta = dtEnd - dtStart  # current UTC time minus last time in file
2710        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2711
2712        # calculate history length in candles:
2713        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2714        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2715            length += 1  # to avoid fraction time
2716
2717        # calculate data blocks count:
2718        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2719
2720        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2721        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2722        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2723        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2724        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2725
2726        tempOld = None  # pandas object for old history, if --only-missing key present
2727        lastTime = None  # datetime object of last old candle in file
2728
2729        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2730            uLogger.debug("--only-missing key present, add only last missing candles...")
2731            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2732
2733            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2734
2735            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2736            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2737            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2738            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2739
2740            # get last datetime object from last string in file or minus 1 delta if file is empty:
2741            if len(tempOld) > 0:
2742                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2743
2744            else:
2745                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2746
2747            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2748
2749        responseJSONs = []  # raw history blocks of data
2750
2751        blockEnd = dtEnd
2752        for item in range(blocks):
2753            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2754            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2755
2756            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2757                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2758            ))
2759
2760            if blockStart == blockEnd:
2761                uLogger.debug("Skipped this zero-length block...")
2762
2763            else:
2764                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2765                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2766                self.body = str({
2767                    "figi": self.figi,
2768                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2769                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2770                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2771                })
2772                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2773
2774                if "code" in responseJSON.keys():
2775                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2776
2777                else:
2778                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2779                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2780
2781                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2782
2783            blockEnd = blockStart
2784
2785        printCount = len(responseJSONs)  # candles to show in console
2786        if responseJSONs:
2787            tempHistory = pd.DataFrame(
2788                data={
2789                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2790                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2791                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2792                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2793                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2794                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2795                    "volume": [int(item["volume"]) for item in responseJSONs],
2796                },
2797                index=range(len(responseJSONs)),
2798                columns=["date", "time", "open", "high", "low", "close", "volume"],
2799            )
2800            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2801            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2802
2803            # append only newest candles to old history if --only-missing key present:
2804            if onlyMissing and tempOld is not None and lastTime is not None:
2805                index = 0  # find start index in tempHistory data:
2806
2807                for i, item in tempHistory.iterrows():
2808                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2809
2810                    if curTime == lastTime:
2811                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2812                        index = i
2813                        printCount = index + 1
2814                        break
2815
2816                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2817
2818            else:
2819                history = tempHistory  # if no `--only-missing` key then load full data from server
2820
2821            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2822
2823        if history is not None and not history.empty:
2824            if show:
2825                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2826                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2827                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2828                ))
2829
2830        else:
2831            uLogger.warning("Received an empty candles history!")
2832
2833        if self.historyFile is not None:
2834            if history is not None and not history.empty:
2835                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2836                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2837
2838            else:
2839                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2840
2841        else:
2842            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2843
2844        return history
2845
2846    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2847        """
2848        Load candles history from csv-file and return Pandas DataFrame object.
2849
2850        See also: `History()` and `ShowHistoryChart()` methods.
2851
2852        :param filePath: path to csv-file to open.
2853        """
2854        loadedHistory = None  # init candles data object
2855
2856        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2857
2858        if os.path.exists(filePath):
2859            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2860
2861            tfStr = self.priceModel.FormattedDelta(
2862                self.priceModel.timeframe,
2863                "{days} days {hours}h {minutes}m {seconds}s",
2864            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2865                self.priceModel.timeframe,
2866                "{hours}h {minutes}m {seconds}s",
2867            )
2868
2869            if loadedHistory is not None and not loadedHistory.empty:
2870                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2871                    len(loadedHistory),
2872                    tfStr,
2873                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2874                )
2875
2876            else:
2877                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2878
2879        else:
2880            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2881
2882        return loadedHistory
2883
2884    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2885        """
2886        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2887
2888        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2889        Default: `index.html` (both for interact and non-interact candlesticks chart).
2890
2891        See also: `History()` and `LoadHistory()` methods.
2892
2893        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2894        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2895                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2896                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2897                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2898        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2899                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2900        """
2901        if isinstance(candles, str):
2902            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2903            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2904
2905        elif isinstance(candles, pd.DataFrame):
2906            self.priceModel.prices = candles  # set candles chain from variable
2907            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2908
2909            if "datetime" not in candles.columns:
2910                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2911
2912        else:
2913            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2914            raise Exception("Incorrect value")
2915
2916        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2917
2918        if interact:
2919            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2920
2921            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2922
2923        else:
2924            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2925
2926            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2927
2928        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2929
2930    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2931        """
2932        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2933        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2934
2935        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2936
2937        :param operation: string "Buy" or "Sell".
2938        :param lots: volume, integer count of lots >= 1.
2939        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2940        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2941        :param expDate: string "Undefined" by default or local date in future,
2942                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2943        :return: JSON with response from broker server.
2944        """
2945        if self.accountId is None or not self.accountId:
2946            uLogger.error("Variable `accountId` must be defined for using this method!")
2947            raise Exception("Account ID required")
2948
2949        if operation is None or not operation or operation not in ("Buy", "Sell"):
2950            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2951            raise Exception("Incorrect value")
2952
2953        if lots is None or lots < 1:
2954            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2955            lots = 1
2956
2957        if tp is None or tp < 0:
2958            tp = 0
2959
2960        if sl is None or sl < 0:
2961            sl = 0
2962
2963        if expDate is None or not expDate:
2964            expDate = "Undefined"
2965
2966        if not (self.ticker or self.figi):
2967            uLogger.error("Ticker or FIGI must be defined!")
2968            raise Exception("Ticker or FIGI required")
2969
2970        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2971        self.ticker = instrument["ticker"]
2972        self.figi = instrument["figi"]
2973
2974        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2975
2976        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2977        self.body = str({
2978            "figi": self.figi,
2979            "quantity": str(lots),
2980            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2981            "accountId": str(self.accountId),
2982            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2983        })
2984        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2985
2986        if "orderId" in response.keys():
2987            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2988                operation, response["orderId"],
2989                self.ticker, self.figi, lots,
2990                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2991                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2992                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2993            ))
2994
2995            if tp > 0:
2996                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2997
2998            if sl > 0:
2999                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3000
3001        else:
3002            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3003
3004        return response
3005
3006    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3007        """
3008        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3009        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3010
3011        See also: `Order()` and `Trade()` docstrings.
3012
3013        :param lots: volume, integer count of lots >= 1.
3014        :param tp: float > 0, take profit price of stop-order.
3015        :param sl: float > 0, stop loss price of stop-order.
3016        :param expDate: it's a local date in future.
3017                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3018        :return: JSON with response from broker server.
3019        """
3020        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3021
3022    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3023        """
3024        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3025        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3026
3027        See also: `Order()` and `Trade()` docstrings.
3028
3029        :param lots: volume, integer count of lots >= 1.
3030        :param tp: float > 0, take profit price of stop-order.
3031        :param sl: float > 0, stop loss price of stop-order.
3032        :param expDate: it's a local date in the future.
3033                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3034        :return: JSON with response from broker server.
3035        """
3036        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3037
3038    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3039        """
3040        Close position of given instruments.
3041
3042        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3043        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3044                         This avoids unnecessary downloading data from the server.
3045        """
3046        if instruments is None or not instruments:
3047            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3048            raise Exception("Ticker or FIGI required")
3049
3050        if isinstance(instruments, str):
3051            instruments = [instruments]
3052
3053        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3054        if uniqueInstruments:
3055            if portfolio is None or not portfolio:
3056                portfolio = self.Overview(show=False)
3057
3058            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3059            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3060
3061            for self.figi in uniqueInstruments:
3062                if self.figi not in allOpened:
3063                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3064                    continue
3065
3066                # search open trade info about instrument by ticker:
3067                instrument = {}
3068                for iType in TKS_INSTRUMENTS:
3069                    if instrument:
3070                        break
3071
3072                    for item in portfolio["stat"][iType]:
3073                        if item["figi"] == self.figi:
3074                            instrument = item
3075                            break
3076
3077                if instrument:
3078                    self.ticker = instrument["ticker"]
3079                    self.figi = instrument["figi"]
3080
3081                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3082                        self.ticker,
3083                        self.figi,
3084                        int(instrument["volume"]),
3085                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3086                    ))
3087
3088                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3089
3090                    if tradeLots > 0:
3091                        if instrument["blocked"] > 0:
3092                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3093                                instrument["blocked"],
3094                                self.ticker,
3095                                tradeLots,
3096                            ))
3097
3098                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3099                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3100
3101                    else:
3102                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3103
3104    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3105        """
3106        Close all positions of given instruments with defined type.
3107
3108        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3109        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3110                         This avoids unnecessary downloading data from the server.
3111        """
3112        if iType not in TKS_INSTRUMENTS:
3113            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3114
3115        else:
3116            if portfolio is None or not portfolio:
3117                portfolio = self.Overview(show=False)
3118
3119            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3120            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3121
3122            if tickers and portfolio:
3123                self.CloseTrades(tickers, portfolio)
3124
3125            else:
3126                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3127
3128    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3129        """
3130        Universal method to create market or limit orders with all available parameters for current `accountId`.
3131        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3132
3133        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3134        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3135
3136        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3137        then broker immediately open market order as you can do simple --buy or --sell operations!
3138
3139        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3140        When current price will go up or down to target price value then broker opens a limit order.
3141        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3142
3143        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3144
3145        :param operation: string "Buy" or "Sell".
3146        :param orderType: string "Limit" or "Stop".
3147        :param lots: volume, integer count of lots >= 1.
3148        :param targetPrice: target price > 0. This is open trade price for limit order.
3149        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3150                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3151        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3152                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3153                         Stop loss order always executed by market price.
3154        :param expDate: string "Undefined" by default or local date in future.
3155                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3156                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3157                        A limit order has no expiration date, it lasts until the end of the trading day.
3158        :return: JSON with response from broker server.
3159        """
3160        if self.accountId is None or not self.accountId:
3161            uLogger.error("Variable `accountId` must be defined for using this method!")
3162            raise Exception("Account ID required")
3163
3164        if operation is None or not operation or operation not in ("Buy", "Sell"):
3165            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3166            raise Exception("Incorrect value")
3167
3168        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3169            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3170            raise Exception("Incorrect value")
3171
3172        if lots is None or lots < 1:
3173            uLogger.error("You must define trade volume > 0: integer count of lots!")
3174            raise Exception("Incorrect value")
3175
3176        if targetPrice is None or targetPrice <= 0:
3177            uLogger.error("Target price for limit-order must be greater than 0!")
3178            raise Exception("Incorrect value")
3179
3180        if limitPrice is None or limitPrice <= 0:
3181            limitPrice = targetPrice
3182
3183        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3184            stopType = "Limit"
3185
3186        if expDate is None or not expDate:
3187            expDate = "Undefined"
3188
3189        if not (self.ticker or self.figi):
3190            uLogger.error("Tocker or FIGI must be defined!")
3191            raise Exception("Ticker or FIGI required")
3192
3193        response = {}
3194        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3195        self.ticker = instrument["ticker"]
3196        self.figi = instrument["figi"]
3197
3198        if orderType == "Limit":
3199            uLogger.debug(
3200                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3201                    self.ticker, self.figi,
3202                    operation, lots, targetPrice, instrument["currency"],
3203                ))
3204
3205            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3206            self.body = str({
3207                "figi": self.figi,
3208                "quantity": str(lots),
3209                "price": FloatToNano(targetPrice),
3210                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3211                "accountId": str(self.accountId),
3212                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3213            })
3214            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3215
3216            if "orderId" in response.keys():
3217                uLogger.info(
3218                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3219                        response["orderId"],
3220                        self.ticker, self.figi,
3221                        operation, lots, targetPrice, instrument["currency"],
3222                    ))
3223
3224                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3225                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3226                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3227                            targetPrice, instrument["currency"],
3228                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3229                        ))
3230
3231                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3232                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237            else:
3238                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3239
3240        if orderType == "Stop":
3241            uLogger.debug(
3242                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3243                    self.ticker, self.figi,
3244                    operation, lots,
3245                    targetPrice, instrument["currency"],
3246                    limitPrice, instrument["currency"],
3247                    stopType, expDate,
3248                ))
3249
3250            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3251            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3252            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3253
3254            body = {
3255                "figi": self.figi,
3256                "quantity": str(lots),
3257                "price": FloatToNano(limitPrice),
3258                "stopPrice": FloatToNano(targetPrice),
3259                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3260                "accountId": str(self.accountId),
3261                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3262                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3263            }
3264
3265            if expDateUTC:
3266                body["expireDate"] = expDateUTC
3267
3268            self.body = str(body)
3269            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3270
3271            if "stopOrderId" in response.keys():
3272                uLogger.info(
3273                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3274                        response["stopOrderId"],
3275                        self.ticker, self.figi,
3276                        operation, lots,
3277                        targetPrice, instrument["currency"],
3278                        limitPrice, instrument["currency"],
3279                        TKS_STOP_ORDER_TYPES[stopOrderType],
3280                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3281                    ))
3282
3283                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3284                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3285                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3286                            targetPrice, instrument["currency"],
3287                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3288                        ))
3289
3290                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3291                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3292                            targetPrice, instrument["currency"],
3293                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3294                        ))
3295
3296            else:
3297                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3298
3299        return response
3300
3301    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3302        """
3303        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3304        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3305        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3306        See also: `Order()` docstring.
3307
3308        :param lots: volume, integer count of lots >= 1.
3309        :param targetPrice: target price > 0. This is open trade price for limit order.
3310        :return: JSON with response from broker server.
3311        """
3312        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3313
3314    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3315        """
3316        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3317        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3318        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3319        target price value then broker opens a limit order. See also: `Order()` docstring.
3320
3321        :param lots: volume, integer count of lots >= 1.
3322        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3323        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3324                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3325        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3326                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3327        :param expDate: string "Undefined" by default or local date in future.
3328                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3329                        This date is converting to UTC format for server.
3330        :return: JSON with response from broker server.
3331        """
3332        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3333
3334    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3335        """
3336        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3337        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3338        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3339        See also: `Order()` docstring.
3340
3341        :param lots: volume, integer count of lots >= 1.
3342        :param targetPrice: target price > 0. This is open trade price for limit order.
3343        :return: JSON with response from broker server.
3344        """
3345        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3346
3347    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3348        """
3349        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3350        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3351        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3352        target price value then broker opens a limit order. See also: `Order()` docstring.
3353
3354        :param lots: volume, integer count of lots >= 1.
3355        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3356        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3357                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3358        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3359                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3360        :param expDate: string "Undefined" by default or local date in future.
3361                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3362                        This date is converting to UTC format for server.
3363        :return: JSON with response from broker server.
3364        """
3365        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3366
3367    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3368        """
3369        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3370
3371        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3372        :param allOrdersIDs: pre-received lists of all active pending orders.
3373                             This avoids unnecessary downloading data from the server.
3374        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3375        """
3376        if self.accountId is None or not self.accountId:
3377            uLogger.error("Variable `accountId` must be defined for using this method!")
3378            raise Exception("Account ID required")
3379
3380        if orderIDs:
3381            if allOrdersIDs is None or not allOrdersIDs:
3382                rawOrders = self.RequestPendingOrders()
3383                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3384
3385            if allStopOrdersIDs is None or not allStopOrdersIDs:
3386                rawStopOrders = self.RequestStopOrders()
3387                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3388
3389            for orderID in orderIDs:
3390                idInPendingOrders = orderID in allOrdersIDs
3391                idInStopOrders = orderID in allStopOrdersIDs
3392
3393                if not (idInPendingOrders or idInStopOrders):
3394                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3395                    continue
3396
3397                else:
3398                    if idInPendingOrders:
3399                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3400
3401                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3402                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3403                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3404                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3405
3406                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3407                            if self.moreDebug:
3408                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3409
3410                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3411
3412                        else:
3413                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3414
3415                    elif idInStopOrders:
3416                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3417
3418                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3419                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3420                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3421                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3422
3423                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3424                            if self.moreDebug:
3425                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3426
3427                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3428
3429                        else:
3430                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3431
3432                    else:
3433                        continue
3434
3435    def CloseAllOrders(self) -> None:
3436        """
3437        Gets a list of open pending and stop orders and cancel it all.
3438        """
3439        rawOrders = self.RequestPendingOrders()
3440        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3441        lenOrders = len(allOrdersIDs)
3442
3443        rawStopOrders = self.RequestStopOrders()
3444        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3445        lenSOrders = len(allStopOrdersIDs)
3446
3447        if lenOrders > 0 or lenSOrders > 0:
3448            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3449
3450            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3451
3452        else:
3453            uLogger.info("Orders not found, nothing to cancel.")
3454
3455    def CloseAll(self, *args) -> None:
3456        """
3457        Close all available (not blocked) opened trades and orders.
3458
3459        Also, you can select one or more keywords case-insensitive:
3460        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3461
3462        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3463        """
3464        overview = self.Overview(show=False)  # get all open trades info
3465
3466        if len(args) == 0:
3467            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3468            self.CloseAllOrders()  # close all pending and stop orders
3469
3470            for iType in TKS_INSTRUMENTS:
3471                if iType != "Currencies":
3472                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3473
3474        else:
3475            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3476            lowerArgs = [x.lower() for x in args]
3477
3478            if "orders" in lowerArgs:
3479                self.CloseAllOrders()  # close all pending and stop orders
3480
3481            for iType in TKS_INSTRUMENTS:
3482                if iType.lower() in lowerArgs and iType != "Currencies":
3483                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3484
3485    @staticmethod
3486    def ParseOrderParameters(operation, **inputParameters):
3487        """
3488        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3489
3490        :param operation: string "Buy" or "Sell".
3491        :param inputParameters: this is dict of strings that looks like this
3492               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3493               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3494               "prices" key: one or more prices to open limit-orders
3495               Counts of values in lots and prices lists must be equals!
3496        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3497        """
3498        # TODO: update order grid work with api v2
3499        pass
3500        # uLogger.debug("Input parameters: {}".format(inputParameters))
3501        #
3502        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3503        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3504        #     raise Exception("Incorrect value")
3505        #
3506        # if "l" in inputParameters.keys():
3507        #     inputParameters["lots"] = inputParameters.pop("l")
3508        #
3509        # if "p" in inputParameters.keys():
3510        #     inputParameters["prices"] = inputParameters.pop("p")
3511        #
3512        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3513        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3514        #     raise Exception("Incorrect value")
3515        #
3516        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3517        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3518        #
3519        # if len(lots) != len(prices):
3520        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3521        #     raise Exception("Incorrect value")
3522        #
3523        # uLogger.debug("Extracted parameters for orders:")
3524        # uLogger.debug("lots = {}".format(lots))
3525        # uLogger.debug("prices = {}".format(prices))
3526        #
3527        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3528        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3529        # uLogger.debug("Order parameters: {}".format(result))
3530        #
3531        # return result
3532
3533    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3534        """
3535        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3536
3537        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3538        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3539        """
3540        result = False
3541        msg = "Instrument not defined!"
3542
3543        if portfolio is None or not portfolio:
3544            portfolio = self.Overview(show=False)
3545
3546        if self.ticker:
3547            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3548            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3549
3550            for iType in TKS_INSTRUMENTS:
3551                for instrument in portfolio["stat"][iType]:
3552                    if instrument["ticker"] == self.ticker:
3553                        result = True
3554                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3555                        break
3556
3557        elif self.figi:
3558            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3559            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3560
3561            for iType in TKS_INSTRUMENTS:
3562                for instrument in portfolio["stat"][iType]:
3563                    if instrument["figi"] == self.figi:
3564                        result = True
3565                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3566                        break
3567
3568        else:
3569            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3570
3571        uLogger.debug(msg)
3572
3573        return result
3574
3575    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3576        """
3577        Returns instrument from the user's portfolio if it presents there.
3578        Instrument must be defined by `ticker` (highly priority) or `figi`.
3579
3580        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3581        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3582        """
3583        result = None
3584        msg = "Instrument not defined!"
3585
3586        if portfolio is None or not portfolio:
3587            portfolio = self.Overview(show=False)
3588
3589        if self.ticker:
3590            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3591            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3592
3593            for iType in TKS_INSTRUMENTS:
3594                for instrument in portfolio["stat"][iType]:
3595                    if instrument["ticker"] == self.ticker:
3596                        result = instrument
3597                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3598                        break
3599
3600        elif self.figi:
3601            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3602            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3603
3604            for iType in TKS_INSTRUMENTS:
3605                for instrument in portfolio["stat"][iType]:
3606                    if instrument["figi"] == self.figi:
3607                        result = instrument
3608                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3609                        break
3610
3611        else:
3612            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3613
3614        uLogger.debug(msg)
3615
3616        return result
3617
3618    def RequestLimits(self) -> dict:
3619        """
3620        Method for obtaining the available funds for withdrawal for current `accountId`.
3621
3622        See also:
3623        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3624        - `OverviewLimits()` method
3625
3626        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3627                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3628                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3629                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3630        """
3631        if self.accountId is None or not self.accountId:
3632            uLogger.error("Variable `accountId` must be defined for using this method!")
3633            raise Exception("Account ID required")
3634
3635        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3636
3637        self.body = str({"accountId": self.accountId})
3638        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3639        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3640
3641        if self.moreDebug:
3642            uLogger.debug("Records about available funds for withdrawal successfully received")
3643
3644        return rawLimits
3645
3646    def OverviewLimits(self, show: bool = False) -> dict:
3647        """
3648        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3649
3650        See also: `RequestLimits()`.
3651
3652        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3653        :return: dict with raw parsed data from server and some calculated statistics about it.
3654        """
3655        if self.accountId is None or not self.accountId:
3656            uLogger.error("Variable `accountId` must be defined for using this method!")
3657            raise Exception("Account ID required")
3658
3659        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3660
3661        view = {
3662            "rawLimits": rawLimits,
3663            "limits": {  # parsed data for every currency:
3664                "money": {  # this is an array of portfolio currency positions
3665                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3666                },
3667                "blocked": {  # this is an array of blocked currency
3668                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3669                },
3670                "blockedGuarantee": {  # this is locked money under collateral for futures
3671                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3672                },
3673            },
3674        }
3675
3676        # --- Prepare text table with limits in human-readable format:
3677        if show:
3678            info = [
3679                "# Withdrawal limits\n\n",
3680                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3681                "* **Account ID:** [{}]\n".format(self.accountId),
3682            ]
3683
3684            if view["limits"]["money"]:
3685                info.extend([
3686                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3687                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3688                ])
3689
3690            else:
3691                info.append("\nNo withdrawal limits\n")
3692
3693            for curr in view["limits"]["money"].keys():
3694                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3695                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3696                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3697
3698                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3699                    "[{}]".format(curr),
3700                    "{:.2f}".format(view["limits"]["money"][curr]),
3701                    "{:.2f}".format(availableMoney),
3702                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3703                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3704                )
3705
3706                if curr == "rub":
3707                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3708
3709                else:
3710                    info.append(infoStr)
3711
3712            infoText = "".join(info)
3713
3714            uLogger.info(infoText)
3715
3716            if self.withdrawalLimitsFile:
3717                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3718                    fH.write(infoText)
3719
3720                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3721
3722        return view
3723
3724    def RequestAccounts(self) -> dict:
3725        """
3726        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3727
3728        See also:
3729        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3730        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3731        - `OverviewUserInfo()` method
3732
3733        :return: dict with raw data from server that contains accounts info. Example of dict:
3734                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3735                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3736                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3737                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3738        """
3739        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3740
3741        self.body = str({})
3742        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3743        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3744
3745        if self.moreDebug:
3746            uLogger.debug("Records about available accounts successfully received")
3747
3748        return rawAccounts
3749
3750    def RequestUserInfo(self) -> dict:
3751        """
3752        Method for requesting common user's information.
3753
3754        See also:
3755        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3756        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3757        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3758        - `OverviewUserInfo()` method
3759
3760        :return: dict with raw data from server that contains user's information. Example of dict:
3761                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3762                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3763        """
3764        uLogger.debug("Requesting common user's information. Wait, please...")
3765
3766        self.body = str({})
3767        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3768        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3769
3770        if self.moreDebug:
3771            uLogger.debug("Records about current user successfully received")
3772
3773        return rawUserInfo
3774
3775    def RequestMarginStatus(self, accountId: str = None) -> dict:
3776        """
3777        Method for requesting margin calculation for defined account ID.
3778
3779        See also:
3780        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3781        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3782        - `OverviewUserInfo()` method
3783
3784        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3785        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3786                 Example of responses:
3787                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3788                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3789                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3790                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3791                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3792                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3793        """
3794        if accountId is None or not accountId:
3795            if self.accountId is None or not self.accountId:
3796                uLogger.error("Variable `accountId` must be defined for using this method!")
3797                raise Exception("Account ID required")
3798
3799            else:
3800                accountId = self.accountId  # use `self.accountId` (main ID) by default
3801
3802        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3803
3804        self.body = str({"accountId": accountId})
3805        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3806        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3807
3808        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3809            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3810            rawMargin = {}
3811
3812        else:
3813            if self.moreDebug:
3814                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3815
3816        return rawMargin
3817
3818    def RequestTariffLimits(self) -> dict:
3819        """
3820        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3821
3822        See also:
3823        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3824        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3825        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3826        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3827        - `OverviewUserInfo()` method
3828
3829        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3830                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3831                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3832        """
3833        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3834
3835        self.body = str({})
3836        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3837        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3838
3839        if self.moreDebug:
3840            uLogger.debug("Records with limits of current tariff successfully received")
3841
3842        return rawTariffLimits
3843
3844    def RequestBondCoupons(self, iJSON: dict) -> dict:
3845        """
3846        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3847        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3848        All dates are in UTC timezone.
3849
3850        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3851        Documentation:
3852        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3853        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3854
3855        See also: `ExtendBondsData()`.
3856
3857        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3858                      If raw iJSON is not data of bond then server returns an error [400] with message:
3859                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3860        :return: dictionary with bond payment calendar. Response example
3861                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3862                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3863                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3864                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3865        """
3866        if iJSON["figi"] is None or not iJSON["figi"]:
3867            uLogger.error("FIGI must be defined for using this method!")
3868            raise Exception("FIGI required")
3869
3870        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3871        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3872
3873        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3874            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3875            self.figi,
3876            startDate,
3877            endDate,
3878        ))
3879
3880        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3881        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3882        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3883
3884        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3885            uLogger.warning("Instrument type is not bond!")
3886
3887        else:
3888            if self.moreDebug:
3889                uLogger.debug("Records about bond payment calendar successfully received")
3890
3891        return calendar
3892
3893    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3894        """
3895        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3896        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3897        coupon yields, current yields and some statistics etc.
3898
3899        WARNING! This is too long operation if a lot of bonds requested from broker server.
3900
3901        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3902
3903        :param instruments: list of strings with tickers or FIGIs.
3904        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3905                     for further used by data scientists or stock analytics.
3906        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3907                 In XLSX-file and Pandas DataFrame fields mean:
3908                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3909                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3910        """
3911        if instruments is None or not instruments:
3912            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3913            raise Exception("Ticker or FIGI required")
3914
3915        if isinstance(instruments, str):
3916            instruments = [instruments]
3917
3918        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3919
3920        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3921
3922        iCount = len(uniqueInstruments)
3923        tooLong = iCount >= 20
3924        if tooLong:
3925            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3926
3927        bonds = None
3928        for i, self.figi in enumerate(uniqueInstruments):
3929            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3930
3931            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3932                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3933                rawBond = self.SearchByFIGI(requestPrice=True)
3934
3935                # Widen raw data with UTC current time (iData["actualDateTime"]):
3936                actualDate = datetime.now(tzutc())
3937                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3938
3939                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3940                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3941
3942                # Replace some values with human-readable:
3943                iData["nominalCurrency"] = iData["nominal"]["currency"]
3944                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3945                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3946                iData["aciCurrency"] = iData["aciValue"]["currency"]
3947                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3948                iData["issueSize"] = int(iData["issueSize"])
3949                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3950                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3951                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3952                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3953                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3954                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3955                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3956                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3957                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3958                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3959
3960                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3961                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3962                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3963                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3964                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3965                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3966                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3967                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3968                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3969                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3970                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3971
3972                # Widen raw data with calendar data from `rawCalendar` values:
3973                calendarData = []
3974                if "events" in iData["rawCalendar"].keys():
3975                    for item in iData["rawCalendar"]["events"]:
3976                        calendarData.append({
3977                            "couponDate": item["couponDate"],
3978                            "couponNumber": int(item["couponNumber"]),
3979                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3980                            "payCurrency": item["payOneBond"]["currency"],
3981                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3982                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3983                            "couponStartDate": item["couponStartDate"],
3984                            "couponEndDate": item["couponEndDate"],
3985                            "couponPeriod": item["couponPeriod"],
3986                        })
3987
3988                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3989                    if "maturityDate" not in iData.keys():
3990                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3991
3992                # Widen raw data with Coupon Rate.
3993                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3994                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3995                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3996                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3997
3998                # Widen raw data with Yield to Maturity (YTM) on current date.
3999                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4000                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4001                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4002                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4003                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4004                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4005
4006                iData["calendar"] = calendarData  # adds calendar at the end
4007
4008                # Remove not used data:
4009                iData.pop("uid")
4010                iData.pop("positionUid")
4011                iData.pop("currentPrice")
4012                iData.pop("rawCalendar")
4013
4014                colNames = list(iData.keys())
4015                if bonds is None:
4016                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4017
4018                else:
4019                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4020
4021            else:
4022                uLogger.warning("Instrument is not a bond!")
4023
4024            processed = round(100 * (i + 1) / iCount, 1)
4025            if tooLong and processed % 5 == 0:
4026                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4027
4028            else:
4029                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4030
4031        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4032
4033        # Saving bonds from Pandas DataFrame to XLSX sheet:
4034        if xlsx and self.bondsXLSXFile:
4035            with pd.ExcelWriter(
4036                    path=self.bondsXLSXFile,
4037                    date_format=TKS_DATE_FORMAT,
4038                    datetime_format=TKS_DATE_TIME_FORMAT,
4039                    mode="w",
4040            ) as writer:
4041                bonds.to_excel(
4042                    writer,
4043                    sheet_name="Extended bonds data",
4044                    index=True,
4045                    encoding="UTF-8",
4046                    freeze_panes=(1, 1),
4047                )  # saving as XLSX-file with freeze first row and column as headers
4048
4049            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4050
4051        return bonds
4052
4053    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4054        """
4055        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4056
4057        WARNING! This is too long operation if a lot of bonds requested from broker server.
4058
4059        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4060
4061        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4062                        extended information about bonds: main info, current prices, bond payment calendar,
4063                        coupon yields, current yields and some statistics etc.
4064                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4065        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4066                     for further used by data scientists or stock analytics.
4067        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4068        """
4069        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4070            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4071
4072        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4073
4074        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4075        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4076        calendar = None
4077        for bond in extBonds.iterrows():
4078            for item in bond[1]["calendar"]:
4079                cData = {
4080                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4081                    "couponDate": item["couponDate"],
4082                    "figi": bond[1]["figi"],
4083                    "ticker": bond[1]["ticker"],
4084                    "name": bond[1]["name"],
4085                    "couponNumber": item["couponNumber"],
4086                    "payOneBond": item["payOneBond"],
4087                    "payCurrency": item["payCurrency"],
4088                    "couponType": item["couponType"],
4089                    "couponPeriod": item["couponPeriod"],
4090                    "fixDate": item["fixDate"],
4091                    "couponStartDate": item["couponStartDate"],
4092                    "couponEndDate": item["couponEndDate"],
4093                }
4094
4095                if calendar is None:
4096                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4097
4098                else:
4099                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4100
4101        if calendar is not None:
4102            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4103
4104            # Saving calendar from Pandas DataFrame to XLSX sheet:
4105            if xlsx:
4106                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4107
4108                with pd.ExcelWriter(
4109                        path=xlsxCalendarFile,
4110                        date_format=TKS_DATE_FORMAT,
4111                        datetime_format=TKS_DATE_TIME_FORMAT,
4112                        mode="w",
4113                ) as writer:
4114                    humanReadable = calendar.copy(deep=True)
4115                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4116                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4117                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4118                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4119                    humanReadable.columns = colNames  # human-readable column names
4120
4121                    humanReadable.to_excel(
4122                        writer,
4123                        sheet_name="Bond payments calendar",
4124                        index=False,
4125                        encoding="UTF-8",
4126                        freeze_panes=(1, 2),
4127                    )  # saving as XLSX-file with freeze first row and column as headers
4128
4129                    del humanReadable  # release df in memory
4130
4131                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4132
4133        return calendar
4134
4135    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4136        """
4137        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4138        Also, creates Markdown file with calendar data, `calendar.md` by default.
4139
4140        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4141
4142        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4143                        extended information about bonds: main info, current prices, bond payment calendar,
4144                        coupon yields, current yields and some statistics etc.
4145                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4146        :param show: if `True` then also printing bonds payment calendar to the console,
4147                     otherwise save to file `calendarFile` only. `False` by default.
4148        :return: multilines text in Markdown format with bonds payment calendar as a table.
4149        """
4150        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4151            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4152
4153        infoText = "# Bond payments calendar\n\n"
4154
4155        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4156
4157        if not (calendar is None or calendar.empty):
4158            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4159
4160            info = [
4161                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4162                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4163            ]
4164
4165            newMonth = False
4166            notOneBond = calendar["figi"].nunique() > 1
4167            for i, bond in enumerate(calendar.iterrows()):
4168                if newMonth and notOneBond:
4169                    info.append(splitLine)
4170
4171                info.append(
4172                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4173                        "  √" if bond[1]["paid"] else "  —",
4174                        bond[1]["couponDate"].split("T")[0],
4175                        bond[1]["figi"],
4176                        bond[1]["ticker"],
4177                        bond[1]["couponNumber"],
4178                        "{} {}".format(
4179                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4180                            bond[1]["payCurrency"],
4181                        ),
4182                        bond[1]["couponType"],
4183                        bond[1]["couponPeriod"],
4184                        bond[1]["fixDate"].split("T")[0],
4185                    )
4186                )
4187
4188                if i < len(calendar.values) - 1:
4189                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4190                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4191                    newMonth = False if curDate.month == nextDate.month else True
4192
4193                else:
4194                    newMonth = False
4195
4196            infoText += "".join(info)
4197
4198            if show:
4199                uLogger.info("{}".format(infoText))
4200
4201            if self.calendarFile is not None:
4202                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4203                    fH.write(infoText)
4204
4205                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4206
4207        else:
4208            infoText += "No data\n"
4209
4210        return infoText
4211
4212    def OverviewAccounts(self, show: bool = False) -> dict:
4213        """
4214        Method for parsing and show simple table with all available user accounts.
4215
4216        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4217
4218        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4219        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4220                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4221                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4222                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4223                                                        "closed": "—", "access": "Full access" }, ...}}`
4224        """
4225        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4226
4227        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4228        accounts = {
4229            item["id"]: {
4230                "type": TKS_ACCOUNT_TYPES[item["type"]],
4231                "name": item["name"],
4232                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4233                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4234                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4235                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4236            } for item in rawAccounts["accounts"]
4237        }
4238
4239        # Raw and parsed data with some fields replaced in "stat" section:
4240        view = {
4241            "rawAccounts": rawAccounts,
4242            "stat": accounts,
4243        }
4244
4245        # --- Prepare simple text table with only accounts data in human-readable format:
4246        if show:
4247            info = [
4248                "# User accounts\n\n",
4249                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4250                "| Account ID   | Type                      | Status                    | Name                           |\n",
4251                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4252            ]
4253
4254            for account in view["stat"].keys():
4255                info.extend([
4256                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4257                        account,
4258                        view["stat"][account]["type"],
4259                        view["stat"][account]["status"],
4260                        view["stat"][account]["name"],
4261                    )
4262                ])
4263
4264            infoText = "".join(info)
4265
4266            uLogger.info(infoText)
4267
4268            if self.userAccountsFile:
4269                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4270                    fH.write(infoText)
4271
4272                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4273
4274        return view
4275
4276    def OverviewUserInfo(self, show: bool = False) -> dict:
4277        """
4278        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4279
4280        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4281
4282        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4283        :return: dict with raw parsed data from server and some calculated statistics about it.
4284        """
4285        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4286        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4287        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4288        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4289        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4290        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4291
4292        # This is dict with parsed common user data:
4293        userInfo = {
4294            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4295            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4296            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4297            "tariff": rawUserInfo["tariff"],
4298        }
4299
4300        # This is an array of dict with parsed margin statuses for every account IDs:
4301        margins = {}
4302        for accountId in accounts.keys():
4303            if rawMargins[accountId]:
4304                margins[accountId] = {
4305                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4306                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4307                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4308                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4309                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4310                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4311                }
4312
4313            else:
4314                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4315
4316        unary = {}  # unary-connection limits
4317        for item in rawTariffLimits["unaryLimits"]:
4318            if item["limitPerMinute"] in unary.keys():
4319                unary[item["limitPerMinute"]].extend(item["methods"])
4320
4321            else:
4322                unary[item["limitPerMinute"]] = item["methods"]
4323
4324        stream = {}  # stream-connection limits
4325        for item in rawTariffLimits["streamLimits"]:
4326            if item["limit"] in stream.keys():
4327                stream[item["limit"]].extend(item["streams"])
4328
4329            else:
4330                stream[item["limit"]] = item["streams"]
4331
4332        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4333        limits = {
4334            "unary": unary,
4335            "stream": stream,
4336        }
4337
4338        # Raw and parsed data as an output result:
4339        view = {
4340            "rawUserInfo": rawUserInfo,
4341            "rawAccounts": rawAccounts,
4342            "rawMargins": rawMargins,
4343            "rawTariffLimits": rawTariffLimits,
4344            "stat": {
4345                "userInfo": userInfo,
4346                "accounts": accounts,
4347                "margins": margins,
4348                "limits": limits,
4349            },
4350        }
4351
4352        # --- Prepare text table with user information in human-readable format:
4353        if show:
4354            info = [
4355                "# Full user information\n\n",
4356                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4357                "## Common information\n\n",
4358                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4359                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4360                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4361                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4362                "\n## User accounts\n\n",
4363            ]
4364
4365            for account in view["stat"]["accounts"].keys():
4366                info.extend([
4367                    "### ID: [{}]\n\n".format(account),
4368                    "| Parameters           | Values                                                       |\n",
4369                    "|----------------------|--------------------------------------------------------------|\n",
4370                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4371                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4372                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4373                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4374                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4375                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4376                ])
4377
4378                if margins[account]:
4379                    info.extend([
4380                        "| Margin status:       | Enabled                                                      |\n",
4381                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4382                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4383                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4384                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4385                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4386                    ])
4387
4388                else:
4389                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4390
4391            info.extend([
4392                "\n## Current user tariff limits\n",
4393                "\nSee also:\n",
4394                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4395                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4396                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4397                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4398                "\n### Unary limits\n",
4399            ])
4400
4401            if unary:
4402                for key, values in sorted(unary.items()):
4403                    info.append("\n* Max requests per minute: {}\n".format(key))
4404
4405                    for value in values:
4406                        info.append("  - {}\n".format(value))
4407
4408            else:
4409                info.append("\nNot available\n")
4410
4411            info.append("\n### Stream limits\n")
4412
4413            if stream:
4414                for key, values in sorted(stream.items()):
4415                    info.append("\n* Max stream connections: {}\n".format(key))
4416
4417                    for value in values:
4418                        info.append("  - {}\n".format(value))
4419
4420            else:
4421                info.append("\nNot available\n")
4422
4423            infoText = "".join(info)
4424
4425            uLogger.info(infoText)
4426
4427            if self.userInfoFile:
4428                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4429                    fH.write(infoText)
4430
4431                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4432
4433        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
253
254        See also: `SearchByTicker()`, `SearchInstruments()`.
255        """
256
257        self.figi = ""
258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
259
260        See also: `SearchByFIGI()`, `SearchInstruments()`.
261        """
262
263        self.depth = 1
264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
265
266        See also: `GetCurrentPrices()`.
267        """
268
269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
271
272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
273        """
274
275        uLogger.debug("Broker API server: {}".format(self.server))
276
277        self.timeout = 15
278        """Server operations timeout in seconds. Default: `15`.
279
280        See also: `SendAPIRequest()`.
281        """
282
283        self.headers = {
284            "Content-Type": "application/json",
285            "accept": "application/json",
286            "Authorization": "Bearer {}".format(self.token),
287            "x-app-name": "Tim55667757.TKSBrokerAPI",
288        }
289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
290
291        See also: `SendAPIRequest()`.
292        """
293
294        self.body = None
295        """Request body which send to broker server. Default: `None`.
296
297        See also: `SendAPIRequest()`.
298        """
299
300        self.moreDebug = False
301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
302
303        self.historyFile = None
304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
305
306        See also: `History()`.
307        """
308
309        self.htmlHistoryFile = "index.html"
310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
311
312        See also: `ShowHistoryChart()`.
313        """
314
315        self.instrumentsFile = "instruments.md"
316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
317
318        See also: `ShowInstrumentsInfo()`.
319        """
320
321        self.searchResultsFile = "search-results.md"
322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
323
324        See also: `SearchInstruments()`.
325        """
326
327        self.pricesFile = "prices.md"
328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
329
330        See also: `GetListOfPrices()`.
331        """
332
333        self.infoFile = "info.md"
334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
335
336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
337        """
338
339        self.bondsXLSXFile = "ext-bonds.xlsx"
340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
342
343        See also: `ExtendBondsData()`.
344        """
345
346        self.calendarFile = "calendar.md"
347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
348        
349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
350
351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
352        """
353
354        self.overviewFile = "overview.md"
355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
356
357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
358        """
359
360        self.overviewDigestFile = "overview-digest.md"
361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
362
363        See also: `Overview()` with parameter `details="digest"`.
364        """
365
366        self.overviewPositionsFile = "overview-positions.md"
367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
368
369        See also: `Overview()` with parameter `details="positions"`.
370        """
371
372        self.overviewOrdersFile = "overview-orders.md"
373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
374
375        See also: `Overview()` with parameter `details="orders"`.
376        """
377
378        self.overviewAnalyticsFile = "overview-analytics.md"
379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
380
381        See also: `Overview()` with parameter `details="analytics"`.
382        """
383
384        self.overviewBondsCalendarFile = "overview-calendar.md"
385        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
386
387        See also: `Overview()` with parameter `details="calendar"`.
388        """
389
390        self.reportFile = "deals.md"
391        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
392
393        See also: `Deals()`.
394        """
395
396        self.withdrawalLimitsFile = "limits.md"
397        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
398
399        See also: `OverviewLimits()` and `RequestLimits()`.
400        """
401
402        self.userInfoFile = "user-info.md"
403        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
404
405        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
406        """
407
408        self.userAccountsFile = "accounts.md"
409        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
410
411        See also: `OverviewAccounts()`, `RequestAccounts()`.
412        """
413
414        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
415        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
416
417        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
418
419        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
420        """
421
422        self.iList = None  # init iList for raw instruments data
423        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
424        
425        See also: `Listing()`, `DumpInstruments()`.
426        """
427
428        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
429        if useCache:
430            if os.path.exists(self.iListDumpFile):
431                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
432                curTime = datetime.now(tzutc())
433
434                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
435                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
436
437                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
438
439                else:
440                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
441
442                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
443                        os.path.abspath(self.iListDumpFile),
444                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
445                    ))
446
447            else:
448                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
449                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
450
451        else:
452            self.iList = self.Listing()  # request new raw instruments data from broker server
453            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
454
455        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
456        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
457
458        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
459        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
475    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
476        """
477        Send GET or POST request to broker server and receive JSON object.
478
479        self.header: must be defining with dictionary of headers.
480        self.body: if define then used as request body. None by default.
481        self.timeout: global request timeout, 15 seconds by default.
482        :param url: url with REST request.
483        :param reqType: send "GET" or "POST" request. "GET" by default.
484        :param retry: how many times retry after first request if an 5xx server errors occurred.
485        :param pause: sleep time in seconds between retries.
486        :return: response JSON (dictionary) from broker.
487        """
488        if reqType not in ("GET", "POST"):
489            uLogger.error("You can define request type: 'GET' or 'POST'!")
490            raise Exception("Incorrect value")
491
492        if self.moreDebug:
493            uLogger.debug("Request parameters:")
494            uLogger.debug("    - REST API URL: {}".format(url))
495            uLogger.debug("    - request type: {}".format(reqType))
496            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
497            uLogger.debug("    - body:\n{}".format(self.body))
498
499        # fast hack to avoid all operations with some tickers/FIGI
500        responseJSON = {}
501        oK = True
502        for item in self.exclude:
503            if item in url:
504                if self.moreDebug:
505                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
506
507                oK = False
508                break
509
510        if oK:
511            counter = 0
512            response = None
513            errMsg = ""
514
515            while not response and counter <= retry:
516                if reqType == "GET":
517                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
518
519                if reqType == "POST":
520                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
521
522                if self.moreDebug:
523                    uLogger.debug("Response:")
524                    uLogger.debug("    - status code: {}".format(response.status_code))
525                    uLogger.debug("    - reason: {}".format(response.reason))
526                    uLogger.debug("    - body length: {}".format(len(response.text)))
527                    uLogger.debug("    - headers:\n{}".format(response.headers))
528
529                # Server returns some headers:
530                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
531                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
532                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
533                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
534                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
535                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
536                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
537                    sleep(rateLimitWait)
538
539                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
540                if 400 <= response.status_code < 500:
541                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
542                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
543                    counter = retry + 1
544
545                if 500 <= response.status_code < 600:
546                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
547                    uLogger.debug("    - not oK, {}".format(errMsg))
548                    counter += 1
549
550                    if counter <= retry:
551                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
552                        sleep(pause)
553
554            responseJSON = self._ParseJSON(rawData=response.text)
555
556            if errMsg:
557                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
558                uLogger.error("    - not oK, {}".format(errMsg))
559
560        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
593    def Listing(self) -> dict:
594        """
595        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
596
597        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
598        """
599        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
600        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
601
602        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
603        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
604        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
605
606        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
607        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
608        poolUpdater.close()
609
610        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
611        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
612        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
613
614        # calculate minimum price increment (step) for all instruments and set up instrument's type:
615        for iType in iList.keys():
616            for ticker in iList[iType]:
617                iList[iType][ticker]["type"] = iType
618
619                if "minPriceIncrement" in iList[iType][ticker].keys():
620                    iList[iType][ticker]["step"] = NanoToFloat(
621                        iList[iType][ticker]["minPriceIncrement"]["units"],
622                        iList[iType][ticker]["minPriceIncrement"]["nano"],
623                    )
624
625                else:
626                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
627
628        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
630    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
631        """
632        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
633
634        See also: `DumpInstruments()`, `Listing()`.
635
636        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
637                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
638        """
639        if self.iListDumpFile is None or not self.iListDumpFile:
640            uLogger.error("Output name of dump file must be defined!")
641            raise Exception("Filename required")
642
643        if not self.iList or forceUpdate:
644            self.iList = self.Listing()
645
646        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
647
648        # Save as XLSX with separated sheets for every type of instruments:
649        with pd.ExcelWriter(
650                path=xlsxDumpFile,
651                date_format=TKS_DATE_FORMAT,
652                datetime_format=TKS_DATE_TIME_FORMAT,
653                mode="w",
654        ) as writer:
655            for iType in TKS_INSTRUMENTS:
656                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
657                df = df[sorted(df)]  # sorted by column names
658                df = df.applymap(
659                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
660                    na_action="ignore",
661                )  # converting numbers from nano-type to float in every cell
662                df.to_excel(
663                    writer,
664                    sheet_name=iType,
665                    encoding="UTF-8",
666                    freeze_panes=(1, 1),
667                )  # saving as XLSX-file with freeze first row and column as headers
668
669        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
671    def DumpInstruments(self, forceUpdate: bool = True) -> str:
672        """
673        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
674        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
675
676        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
677
678        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
679                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
680        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
681        """
682        if self.iListDumpFile is None or not self.iListDumpFile:
683            uLogger.error("Output name of dump file must be defined!")
684            raise Exception("Filename required")
685
686        if not self.iList or forceUpdate:
687            self.iList = self.Listing()
688
689        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
690        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
691            fH.write(jsonDump)
692
693        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
694
695        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
697    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
698        """
699        Show information about one instrument defined by json data and prints it in Markdown format.
700
701        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
702
703        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
704        :param show: if `True` then also printing information about instrument and its current price.
705        :return: multilines text in Markdown format with information about one instrument.
706        """
707        splitLine = "|                                                             |                                                        |\n"
708        infoText = ""
709
710        if iJSON is not None and iJSON and isinstance(iJSON, dict):
711            info = [
712                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
713                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
714                "| Parameters                                                  | Values                                                 |\n",
715                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
716                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
717                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
718            ]
719
720            if "sector" in iJSON.keys() and iJSON["sector"]:
721                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
722
723            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
724                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
725                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
726            )))
727
728            info.extend([
729                splitLine,
730                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
731                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
732            ])
733
734            if "isin" in iJSON.keys() and iJSON["isin"]:
735                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
736
737            if "classCode" in iJSON.keys():
738                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
739
740            info.extend([
741                splitLine,
742                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
743                splitLine,
744                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
745                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
746                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
747            ])
748
749            if iJSON["figi"]:
750                self.figi = iJSON["figi"]
751                iJSON = iJSON | self.RequestTradingStatus()
752
753                info.extend([
754                    splitLine,
755                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
756                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
757                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
758                ])
759
760            info.append(splitLine)
761
762            if "type" in iJSON.keys() and iJSON["type"]:
763                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
764
765            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
766                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
767
768            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
769                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
770
771            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
772                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
773
774            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
775                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
776
777            if "focusType" in iJSON.keys() and iJSON["focusType"]:
778                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
779
780            if "assetType" in iJSON.keys() and iJSON["assetType"]:
781                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
782
783            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
784                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
785
786            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
787                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
788
789            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
790                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
791
792            if "currency" in iJSON.keys():
793                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
794
795            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
796                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
797
798            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
799                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
800
801            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
802                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
803
804            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
805                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
806
807            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
808                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
809
810            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
811                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
812
813            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
814                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
815
816            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
817                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
818
819            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
820                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
821
822            iExt = None
823            if iJSON["type"] == "Bonds":
824                info.extend([
825                    splitLine,
826                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
827                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
828                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
829                        iJSON["nominal"]["currency"],
830                    )),
831                ])
832
833                if "floatingCouponFlag" in iJSON.keys():
834                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
835
836                if "amortizationFlag" in iJSON.keys():
837                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
838
839                info.append(splitLine)
840
841                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
842                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
843
844                if iJSON["figi"]:
845                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
846
847                    info.extend([
848                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
849                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
850                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
851                    ])
852
853                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
854                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
855                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
856                        iJSON["aciValue"]["currency"]
857                    )))
858
859            if "currentPrice" in iJSON.keys():
860                info.append(splitLine)
861
862                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
863                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
864
865                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
866                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
867                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
868                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
869                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
870
871                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
872                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
873
874                info.extend([
875                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
876                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
877                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
878                    )),
879                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
880                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
881                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
882                    )),
883                    "| Changes between last deal price and last close              | {:<54} |\n".format(
884                        "{:.2f}%{}".format(
885                            iJSON["currentPrice"]["changes"],
886                            " ({}{:.2f} {})".format(
887                                "+" if bondChangesDelta > 0 else "",
888                                bondChangesDelta,
889                                aciCurrency
890                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
891                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
892                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
893                                currency
894                            ),
895                        )
896                    ),
897                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
898                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
899                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
900                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
902                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
903                    )),
904                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
905                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
906                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
907                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
908                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
909                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
910                    )),
911                ])
912
913            if "lot" in iJSON.keys():
914                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
915
916            if "step" in iJSON.keys() and iJSON["step"] != 0:
917                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
918
919            # Add bond payment calendar:
920            if iJSON["type"] == "Bonds":
921                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
922                info.extend(["\n", strCalendar])
923
924            infoText += "".join(info)
925
926            if show:
927                uLogger.info("{}".format(infoText))
928
929            else:
930                uLogger.debug("{}".format(infoText))
931
932            if self.infoFile is not None:
933                with open(self.infoFile, "w", encoding="UTF-8") as fH:
934                    fH.write(infoText)
935
936                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
937
938        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 940    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 941        """
 942        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 943
 944        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 945        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 946        :return: JSON formatted data with information about instrument.
 947        """
 948        tickerJSON = {}
 949        if self.moreDebug:
 950            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 951
 952        if not self.ticker:
 953            uLogger.warning("self.ticker variable is not be empty!")
 954
 955        else:
 956            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 957                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 958                raise Exception("Instrument not allowed")
 959
 960            if not self.iList:
 961                self.iList = self.Listing()
 962
 963            if self.ticker in self.iList["Shares"].keys():
 964                tickerJSON = self.iList["Shares"][self.ticker]
 965                if self.moreDebug:
 966                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 967
 968            elif self.ticker in self.iList["Currencies"].keys():
 969                tickerJSON = self.iList["Currencies"][self.ticker]
 970                if self.moreDebug:
 971                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 972
 973            elif self.ticker in self.iList["Bonds"].keys():
 974                tickerJSON = self.iList["Bonds"][self.ticker]
 975                if self.moreDebug:
 976                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 977
 978            elif self.ticker in self.iList["Etfs"].keys():
 979                tickerJSON = self.iList["Etfs"][self.ticker]
 980                if self.moreDebug:
 981                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 982
 983            elif self.ticker in self.iList["Futures"].keys():
 984                tickerJSON = self.iList["Futures"][self.ticker]
 985                if self.moreDebug:
 986                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 987
 988        if tickerJSON:
 989            self.figi = tickerJSON["figi"]
 990
 991            if requestPrice:
 992                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 993
 994                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 995                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 996
 997                else:
 998                    tickerJSON["currentPrice"]["changes"] = 0
 999
1000            if show:
1001                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1002
1003        else:
1004            if show:
1005                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1006
1007        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1009    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1010        """
1011        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :return: JSON formatted data with information about instrument.
1016        """
1017        figiJSON = {}
1018        if self.moreDebug:
1019            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1020
1021        if not self.figi:
1022            uLogger.warning("self.figi variable is not be empty!")
1023
1024        else:
1025            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1026                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1027                raise Exception("Instrument not allowed")
1028
1029            if not self.iList:
1030                self.iList = self.Listing()
1031
1032            for item in self.iList["Shares"].keys():
1033                if self.figi == self.iList["Shares"][item]["figi"]:
1034                    figiJSON = self.iList["Shares"][item]
1035
1036                    if self.moreDebug:
1037                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1038
1039                    break
1040
1041            if not figiJSON:
1042                for item in self.iList["Currencies"].keys():
1043                    if self.figi == self.iList["Currencies"][item]["figi"]:
1044                        figiJSON = self.iList["Currencies"][item]
1045
1046                        if self.moreDebug:
1047                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1048
1049                        break
1050
1051            if not figiJSON:
1052                for item in self.iList["Bonds"].keys():
1053                    if self.figi == self.iList["Bonds"][item]["figi"]:
1054                        figiJSON = self.iList["Bonds"][item]
1055
1056                        if self.moreDebug:
1057                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1058
1059                        break
1060
1061            if not figiJSON:
1062                for item in self.iList["Etfs"].keys():
1063                    if self.figi == self.iList["Etfs"][item]["figi"]:
1064                        figiJSON = self.iList["Etfs"][item]
1065
1066                        if self.moreDebug:
1067                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1068
1069                        break
1070
1071            if not figiJSON:
1072                for item in self.iList["Futures"].keys():
1073                    if self.figi == self.iList["Futures"][item]["figi"]:
1074                        figiJSON = self.iList["Futures"][item]
1075
1076                        if self.moreDebug:
1077                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1078
1079                        break
1080
1081        if figiJSON:
1082            self.figi = figiJSON["figi"]
1083            self.ticker = figiJSON["ticker"]
1084
1085            if requestPrice:
1086                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1087
1088                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1089                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1090
1091                else:
1092                    figiJSON["currentPrice"]["changes"] = 0
1093
1094            if show:
1095                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1096
1097        else:
1098            if show:
1099                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1100
1101        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1103    def GetCurrentPrices(self, show: bool = True) -> dict:
1104        """
1105        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1106        `{"buy": [{"price": 1243.8, "quantity": 193},
1107                  {"price": 1244.0, "quantity": 168},
1108                  {"price": 1244.8, "quantity": 5},
1109                  {"price": 1245.0, "quantity": 61},
1110                  {"price": 1245.4, "quantity": 60}],
1111          "sell": [{"price": 1243.6, "quantity": 8},
1112                   {"price": 1242.6, "quantity": 10},
1113                   {"price": 1242.4, "quantity": 18},
1114                   {"price": 1242.2, "quantity": 50},
1115                   {"price": 1242.0, "quantity": 113}],
1116          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1117        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1118        - sell: list of dicts with Buyers prices,
1119            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1120            - quantity: volume value by current price in lots,
1121        - limitUp: current trade session limit price, maximum,
1122        - limitDown: current trade session limit price, minimum,
1123        - lastPrice: last deal price of the instrument,
1124        - closePrice: previous trade session close price of the instrument.
1125
1126        See also: `SearchByTicker()` and `SearchByFIGI()`.
1127        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1128        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1129
1130        :param show: if `True` then print DOM to log and console.
1131        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1132                 If an error occurred then returns an empty record:
1133                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1134        """
1135        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1136
1137        if self.depth < 1:
1138            uLogger.error("Depth of Market (DOM) must be >=1!")
1139            raise Exception("Incorrect value")
1140
1141        if not (self.ticker or self.figi):
1142            uLogger.error("self.ticker or self.figi variables must be defined!")
1143            raise Exception("Ticker or FIGI required")
1144
1145        if self.ticker and not self.figi:
1146            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1147            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1148
1149        if not self.ticker and self.figi:
1150            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1151            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1152
1153        if not self.figi:
1154            uLogger.error("FIGI is not defined!")
1155            raise Exception("Ticker or FIGI required")
1156
1157        else:
1158            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1159
1160            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1161            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1162            self.body = str({"figi": self.figi, "depth": self.depth})
1163            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1164
1165            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1166                # list of dicts with sellers orders:
1167                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1168
1169                # list of dicts with buyers orders:
1170                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1171
1172                # max price of instrument at this time:
1173                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1174
1175                # min price of instrument at this time:
1176                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1177
1178                # last price of deal with instrument:
1179                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1180
1181                # last close price of instrument:
1182                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1183
1184            else:
1185                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1186                uLogger.debug("Server response: {}".format(pricesResponse))
1187
1188            if show:
1189                if prices["buy"] or prices["sell"]:
1190                    info = [
1191                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1192                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1193                            self.ticker,
1194                            self.figi,
1195                            self.depth,
1196                        ),
1197                        "-" * 60, "\n",
1198                        "             Orders of Buyers | Orders of Sellers\n",
1199                        "-" * 60, "\n",
1200                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1201                        "-" * 60, "\n",
1202                    ]
1203
1204                    if not prices["buy"]:
1205                        info.append("                              | No orders!\n")
1206                        sumBuy = 0
1207
1208                    else:
1209                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1210                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1211                        for item in maxMinSorted:
1212                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1213
1214                    if not prices["sell"]:
1215                        info.append("No orders!                    |\n")
1216                        sumSell = 0
1217
1218                    else:
1219                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1220                        for item in prices["sell"]:
1221                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1222
1223                    info.extend([
1224                        "-" * 60, "\n",
1225                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1226                        "-" * 60, "\n",
1227                    ])
1228
1229                    infoText = "".join(info)
1230
1231                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1232
1233                else:
1234                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1235
1236        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1238    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1239        """
1240        This method get and show information about all available broker instruments for current user account.
1241        If `instrumentsFile` string is not empty then also save information to this file.
1242
1243        :param show: if `True` then print results to console, if `False` — print only to file.
1244        :return: multi-lines string with all available broker instruments
1245        """
1246        if not self.iList:
1247            self.iList = self.Listing()
1248
1249        info = [
1250            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1251            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1252        ]
1253
1254        # add instruments count by type:
1255        for iType in self.iList.keys():
1256            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1257
1258        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1259        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1260
1261        # generating info tables with all instruments by type:
1262        for iType in self.iList.keys():
1263            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1264
1265            for instrument in self.iList[iType].keys():
1266                iName = self.iList[iType][instrument]["name"]  # instrument's name
1267                if len(iName) > 57:
1268                    iName = "{}...".format(iName[:54])  # right trim for a long string
1269
1270                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1271                    self.iList[iType][instrument]["ticker"],
1272                    iName,
1273                    self.iList[iType][instrument]["figi"],
1274                    self.iList[iType][instrument]["currency"],
1275                    self.iList[iType][instrument]["lot"],
1276                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1277                ))
1278
1279        infoText = "".join(info)
1280
1281        if show:
1282            uLogger.info(infoText)
1283
1284        if self.instrumentsFile:
1285            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1286                fH.write(infoText)
1287
1288            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1289
1290        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1292    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1293        """
1294        This method search and show information about instruments by part of its ticker, FIGI or name.
1295        If `searchResultsFile` string is not empty then also save information to this file.
1296
1297        :param pattern: string with part of ticker, FIGI or instrument's name.
1298        :param show: if `True` then print results to console, if `False` — return list of result only.
1299        :return: list of dictionaries with all found instruments.
1300        """
1301        if not self.iList:
1302            self.iList = self.Listing()
1303
1304        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1305        compiledPattern = re.compile(pattern, re.IGNORECASE)
1306
1307        for iType in self.iList:
1308            for instrument in self.iList[iType].values():
1309                searchResult = compiledPattern.search(" ".join(
1310                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1311                ))
1312
1313                if searchResult:
1314                    searchResults[iType][instrument["ticker"]] = instrument
1315
1316        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1317        info = [
1318            "# Search results\n\n",
1319            "* **Search pattern:** [{}]\n".format(pattern),
1320            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1321            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1322        ]
1323        infoShort = info[:]
1324
1325        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1326        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1327        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1328
1329        if resultsLen == 0:
1330            info.append("\nNo results\n")
1331            infoShort.append("\nNo results\n")
1332            uLogger.warning("No results. Try changing your search pattern.")
1333
1334        else:
1335            for iType in searchResults:
1336                iTypeValuesCount = len(searchResults[iType].values())
1337                if iTypeValuesCount > 0:
1338                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340
1341                    for instrument in searchResults[iType].values():
1342                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1343                            instrument["type"],
1344                            instrument["ticker"],
1345                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1346                            instrument["figi"],
1347                        ))
1348
1349                    if iTypeValuesCount <= 5:
1350                        infoShort.extend(info[-iTypeValuesCount:])
1351
1352                    else:
1353                        infoShort.extend(info[-5:])
1354                        infoShort.append(skippedLine)
1355
1356        infoText = "".join(info)
1357        infoTextShort = "".join(infoShort)
1358
1359        if show:
1360            uLogger.info(infoTextShort)
1361            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1362
1363        if self.searchResultsFile:
1364            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1365                fH.write(infoText)
1366
1367            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1368
1369        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1371    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1372        """
1373        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1374
1375        :param instruments: list of strings with tickers or FIGIs.
1376        :return: list with unique instrument FIGIs only.
1377        """
1378        requestedInstruments = []
1379        for iName in instruments:
1380            if iName not in self.aliases.keys():
1381                if iName not in requestedInstruments:
1382                    requestedInstruments.append(iName)
1383
1384            else:
1385                if iName not in requestedInstruments:
1386                    if self.aliases[iName] not in requestedInstruments:
1387                        requestedInstruments.append(self.aliases[iName])
1388
1389        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1390
1391        onlyUniqueFIGIs = []
1392        for iName in requestedInstruments:
1393            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1394                continue
1395
1396            self.ticker = iName
1397            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1398
1399            if not iData:
1400                self.ticker = ""
1401                self.figi = iName
1402
1403                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1404
1405                if not iData:
1406                    self.figi = ""
1407                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1408
1409            if iData and iData["figi"] not in onlyUniqueFIGIs:
1410                onlyUniqueFIGIs.append(iData["figi"])
1411
1412        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1413
1414        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1416    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1417        """
1418        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1419
1420        See limits: https://tinkoff.github.io/investAPI/limits/
1421
1422        If `pricesFile` string is not empty then also save information to this file.
1423
1424        :param instruments: list of strings with tickers or FIGIs.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1427                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1428        """
1429        if instruments is None or not instruments:
1430            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1431            raise Exception("Ticker or FIGI required")
1432
1433        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1434
1435        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1436
1437        iList = []  # trying to get info and current prices about all unique instruments:
1438        for self.figi in onlyUniqueFIGIs:
1439            iData = self.SearchByFIGI(requestPrice=True)
1440            iList.append(iData)
1441
1442        self.ShowListOfPrices(iList, show)
1443
1444        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1446    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1447        """
1448        Show table contains current prices of given instruments.
1449
1450        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1451                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1452        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1453        :return: multilines text in Markdown format as a table contains current prices.
1454        """
1455        infoText = ""
1456
1457        if show or self.pricesFile:
1458            info = [
1459                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1460                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1461                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1462            ]
1463
1464            for item in iList:
1465                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1466                    item["ticker"],
1467                    item["figi"],
1468                    item["type"],
1469                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1470                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1471                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1472                    "{} / {}".format(
1473                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1474                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1475                    ),
1476                    "{} / {}".format(
1477                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1478                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1479                    ),
1480                    item["currency"],
1481                ))
1482
1483            infoText = "".join(info)
1484
1485            if show:
1486                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1487
1488            if self.pricesFile:
1489                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1490                    fH.write(infoText)
1491
1492                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1493
1494        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1496    def RequestTradingStatus(self) -> dict:
1497        """
1498        Requesting trading status for the instrument defined by `figi` variable.
1499
1500        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1501
1502        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1503
1504        :return: dictionary with trading status attributes. Response example:
1505                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1506                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1507        """
1508        if self.figi is None or not self.figi:
1509            uLogger.error("Variable `figi` must be defined for using this method!")
1510            raise Exception("FIGI required")
1511
1512        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1513
1514        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1515        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1516        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1517
1518        if self.moreDebug:
1519            uLogger.debug("Records about current trading status successfully received")
1520
1521        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1523    def RequestPortfolio(self) -> dict:
1524        """
1525        Requesting actual user's portfolio for current `accountId`.
1526
1527        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1528
1529        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1530
1531        :return: dictionary with user's portfolio.
1532        """
1533        if self.accountId is None or not self.accountId:
1534            uLogger.error("Variable `accountId` must be defined for using this method!")
1535            raise Exception("Account ID required")
1536
1537        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1538
1539        self.body = str({"accountId": self.accountId})
1540        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1541        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1542
1543        if self.moreDebug:
1544            uLogger.debug("Records about user's portfolio successfully received")
1545
1546        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1548    def RequestPositions(self) -> dict:
1549        """
1550        Requesting open positions by currencies and instruments for current `accountId`.
1551
1552        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1553
1554        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1555
1556        :return: dictionary with open positions by instruments.
1557        """
1558        if self.accountId is None or not self.accountId:
1559            uLogger.error("Variable `accountId` must be defined for using this method!")
1560            raise Exception("Account ID required")
1561
1562        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1563
1564        self.body = str({"accountId": self.accountId})
1565        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1566        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1567
1568        if self.moreDebug:
1569            uLogger.debug("Records about current open positions successfully received")
1570
1571        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1573    def RequestPendingOrders(self) -> list:
1574        """
1575        Requesting current actual pending orders for current `accountId`.
1576
1577        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1578
1579        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1580
1581        :return: list of dictionaries with pending orders.
1582        """
1583        if self.accountId is None or not self.accountId:
1584            uLogger.error("Variable `accountId` must be defined for using this method!")
1585            raise Exception("Account ID required")
1586
1587        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1588
1589        self.body = str({"accountId": self.accountId})
1590        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1591        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1592
1593        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1594
1595        return rawOrders

Requesting current actual pending orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1597    def RequestStopOrders(self) -> list:
1598        """
1599        Requesting current actual stop orders for current `accountId`.
1600
1601        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1602
1603        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1604
1605        :return: list of dictionaries with stop orders.
1606        """
1607        if self.accountId is None or not self.accountId:
1608            uLogger.error("Variable `accountId` must be defined for using this method!")
1609            raise Exception("Account ID required")
1610
1611        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1612
1613        self.body = str({"accountId": self.accountId})
1614        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1615        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1616
1617        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1618
1619        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1621    def Overview(self, show: bool = False, details: str = "full") -> dict:
1622        """
1623        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1624        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1625        and `overviewBondsCalendarFile` are defined then also save information to file.
1626
1627        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1628        many requests about the state of the portfolio, and then, based on the received data, a large number
1629        of calculation and statistics are collected.
1630
1631        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1632        :param details: how detailed should the information be?
1633        - `full` — shows full available information about portfolio status (by default),
1634        - `positions` — shows only open positions,
1635        - `orders` — shows only sections of open limits and stop orders.
1636        - `digest` — show a short digest of the portfolio status,
1637        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1638        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1639        :return: dictionary with client's raw portfolio and some statistics.
1640        """
1641        if self.accountId is None or not self.accountId:
1642            uLogger.error("Variable `accountId` must be defined for using this method!")
1643            raise Exception("Account ID required")
1644
1645        view = {
1646            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1647                "headers": {},  # list of dictionaries, response headers without "positions" section
1648                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1649                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1650                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1651                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1652                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1653                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1654                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1655                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1656                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1657            },
1658            "stat": {  # --- some statistics calculated using "raw" sections:
1659                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1660                "availableRUB": 0.,  # available rubles (without other currencies)
1661                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1662                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1663                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1664                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1665                "sharesCostRUB": 0.,  # costs of all shares in RUB
1666                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1667                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1668                "futuresCostRUB": 0.,  # costs of all futures in RUB
1669                "Currencies": [],  # list of dictionaries of all currencies statistics
1670                "Shares": [],  # list of dictionaries of all shares statistics
1671                "Bonds": [],  # list of dictionaries of all bonds statistics
1672                "Etfs": [],  # list of dictionaries of all etfs statistics
1673                "Futures": [],  # list of dictionaries of all futures statistics
1674                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1675                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1676                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1677                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1678                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1679            },
1680            "analytics": {  # --- some analytics of portfolio:
1681                "distrByAssets": {},  # portfolio distribution by assets
1682                "distrByCompanies": {},  # portfolio distribution by companies
1683                "distrBySectors": {},  # portfolio distribution by sectors
1684                "distrByCurrencies": {},  # portfolio distribution by currencies
1685                "distrByCountries": {},  # portfolio distribution by countries
1686                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1687            }
1688        }
1689
1690        details = details.lower()
1691        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1692        if details not in availableDetails:
1693            details = "full"
1694            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1695
1696        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1697
1698        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1699        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1700        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1701        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1702
1703        # save response headers without "positions" section:
1704        for key in portfolioResponse.keys():
1705            if key != "positions":
1706                view["raw"]["headers"][key] = portfolioResponse[key]
1707
1708            else:
1709                continue
1710
1711        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1712        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1713        for item in portfolioResponse["positions"]:
1714            if item["instrumentType"] == "currency":
1715                self.figi = item["figi"]
1716                curr = self.SearchByFIGI(requestPrice=False)
1717
1718                # current price of currency in RUB:
1719                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1720                    "name": curr["name"],
1721                    "currentPrice": NanoToFloat(
1722                        item["currentPrice"]["units"],
1723                        item["currentPrice"]["nano"]
1724                    ),
1725                }
1726
1727                view["raw"]["Currencies"].append(item)
1728
1729            elif item["instrumentType"] == "share":
1730                view["raw"]["Shares"].append(item)
1731
1732            elif item["instrumentType"] == "bond":
1733                view["raw"]["Bonds"].append(item)
1734
1735            elif item["instrumentType"] == "etf":
1736                view["raw"]["Etfs"].append(item)
1737
1738            elif item["instrumentType"] == "futures":
1739                view["raw"]["Futures"].append(item)
1740
1741            else:
1742                continue
1743
1744        # how many volume of currencies (by ISO currency name) are blocked:
1745        for item in view["raw"]["positions"]["blocked"]:
1746            blocked = NanoToFloat(item["units"], item["nano"])
1747            if blocked > 0:
1748                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1749
1750        # how many volume of instruments (by FIGI) are blocked:
1751        for item in view["raw"]["positions"]["securities"]:
1752            blocked = int(item["blocked"])
1753            if blocked > 0:
1754                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1755
1756        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1757
1758        if "rub" in allBlocked.keys():
1759            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1760
1761        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1762        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1763        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1764        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1765        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1766        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1767        view["stat"]["portfolioCostRUB"] = sum([
1768            view["stat"]["allCurrenciesCostRUB"],
1769            view["stat"]["sharesCostRUB"],
1770            view["stat"]["bondsCostRUB"],
1771            view["stat"]["etfsCostRUB"],
1772            view["stat"]["futuresCostRUB"],
1773        ])
1774
1775        # --- calculating some portfolio statistics:
1776        byComp = {}  # distribution by companies
1777        bySect = {}  # distribution by sectors
1778        byCurr = {}  # distribution by currencies (include RUB)
1779        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1780        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1781
1782        for item in portfolioResponse["positions"]:
1783            self.figi = item["figi"]
1784            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1785
1786            if instrument:
1787                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1788                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1789
1790                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1791                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1792
1793                else:
1794                    blocked = 0
1795
1796                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1797                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1798                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1799                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1800                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1801                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1802                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1803                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1804                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1805                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1806                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1807                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1808
1809                statData = {
1810                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1811                    "ticker": instrument["ticker"],  # ticker by FIGI
1812                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1813                    "volume": volume,  # available volume of instrument
1814                    "lots": lots,  # volume in lots of instrument
1815                    "direction": direction,  # direction of an instrument's position: short or long
1816                    "blocked": blocked,  # blocked volume of currency or instrument
1817                    "currentPrice": curPrice,  # current instrument's price in basic asset
1818                    "average": average,  # current average position price
1819                    "cost": cost,  # current cost of all volume of instrument in basic asset
1820                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1821                    "costRUB": costRUB,  # cost of instrument in ruble
1822                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1823                    "profit": profit,  # expected profit at current moment
1824                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1825                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1826                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1827                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1828                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1829                    "step": instrument["step"],  # minimum price increment
1830                }
1831
1832                # adding distribution by unique countries:
1833                if statData["country"] not in byCountry.keys():
1834                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1835
1836                else:
1837                    byCountry[statData["country"]]["cost"] += costRUB
1838                    byCountry[statData["country"]]["percent"] += percentCostRUB
1839
1840                if item["instrumentType"] != "currency":
1841                    # adding distribution by unique companies:
1842                    if statData["name"]:
1843                        if statData["name"] not in byComp.keys():
1844                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1845
1846                        else:
1847                            byComp[statData["name"]]["cost"] += costRUB
1848                            byComp[statData["name"]]["percent"] += percentCostRUB
1849
1850                    # adding distribution by unique sectors:
1851                    if statData["sector"] not in bySect.keys():
1852                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1853
1854                    else:
1855                        bySect[statData["sector"]]["cost"] += costRUB
1856                        bySect[statData["sector"]]["percent"] += percentCostRUB
1857
1858                # adding distribution by unique currencies:
1859                if currency not in byCurr.keys():
1860                    byCurr[currency] = {
1861                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1862                        "cost": costRUB,
1863                        "percent": percentCostRUB
1864                    }
1865
1866                else:
1867                    byCurr[currency]["cost"] += costRUB
1868                    byCurr[currency]["percent"] += percentCostRUB
1869
1870                # saving statistics for every instrument:
1871                if item["instrumentType"] == "currency":
1872                    view["stat"]["Currencies"].append(statData)
1873
1874                    # update dict with free funds for trading (total - blocked) by currencies
1875                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1876                    view["stat"]["funds"][currency] = {
1877                        "total": volume,
1878                        "totalCostRUB": costRUB,  # total volume cost in rubles
1879                        "free": volume - blocked,
1880                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1881                    }
1882
1883                elif item["instrumentType"] == "share":
1884                    view["stat"]["Shares"].append(statData)
1885
1886                elif item["instrumentType"] == "bond":
1887                    view["stat"]["Bonds"].append(statData)
1888
1889                elif item["instrumentType"] == "etf":
1890                    view["stat"]["Etfs"].append(statData)
1891
1892                elif item["instrumentType"] == "Futures":
1893                    view["stat"]["Futures"].append(statData)
1894
1895                else:
1896                    continue
1897
1898        # total changes in Russian Ruble:
1899        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1900        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1901        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1902        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1903        view["stat"]["funds"]["rub"] = {
1904            "total": view["stat"]["availableRUB"],
1905            "totalCostRUB": view["stat"]["availableRUB"],
1906            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1907            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1908        }
1909
1910        # --- pending orders sector data:
1911        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1912        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1913
1914        for item in view["raw"]["orders"]:
1915            self.figi = item["figi"]
1916
1917            if item["figi"] not in uniquePendingOrdersFIGIs:
1918                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1919
1920                uniquePendingOrdersFIGIs.append(item["figi"])
1921                uniquePendingOrders[item["figi"]] = instrument
1922
1923            else:
1924                instrument = uniquePendingOrders[item["figi"]]
1925
1926            if instrument:
1927                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1928                orderType = TKS_ORDER_TYPES[item["orderType"]]
1929                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1930                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1931
1932                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1933                if item["direction"] == "ORDER_DIRECTION_BUY":
1934                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1935
1936                else:
1937                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1938
1939                # requested price for order execution:
1940                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1941
1942                # necessary changes in percent to reach target from current price:
1943                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1944
1945                view["stat"]["orders"].append({
1946                    "orderID": item["orderId"],  # orderId number parameter of current order
1947                    "figi": item["figi"],  # FIGI identification
1948                    "ticker": instrument["ticker"],  # ticker name by FIGI
1949                    "lotsRequested": item["lotsRequested"],  # requested lots value
1950                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for order execution in base currency
1953                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1954                    "percentChanges": changes,  # changes in percent to target from current price
1955                    "currency": item["currency"],  # instrument's currency name
1956                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1957                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1958                    "status": orderState,  # order status from TKS_ORDER_STATES
1959                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1960                })
1961
1962        # --- stop orders sector data:
1963        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1964        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["stopOrders"]:
1967            self.figi = item["figi"]
1968
1969            if item["figi"] not in uniqueStopOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniqueStopOrdersFIGIs.append(item["figi"])
1973                uniqueStopOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniqueStopOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1981                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1982
1983                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1984                if "expirationTime" in item.keys():
1985                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1986                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1987
1988                else:
1989                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1990                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1991
1992                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1993                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1994                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1995
1996                else:
1997                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1998
1999                # requested price when stop-order executed:
2000                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2001
2002                # price for limit-order, set up when stop-order executed:
2003                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2004
2005                # necessary changes in percent to reach target from current price:
2006                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2007
2008                view["stat"]["stopOrders"].append({
2009                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2010                    "figi": item["figi"],  # FIGI identification
2011                    "ticker": instrument["ticker"],  # ticker name by FIGI
2012                    "lotsRequested": item["lotsRequested"],  # requested lots value
2013                    "currentPrice": lastPrice,  # current instrument's price for defined action
2014                    "targetPrice": target,  # requested price for stop-order execution in base currency
2015                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2016                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2017                    "percentChanges": changes,  # changes in percent to target from current price
2018                    "currency": item["currency"],  # instrument's currency name
2019                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2020                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2021                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2022                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2023                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2024                })
2025
2026        # --- calculating data for analytics section:
2027        # portfolio distribution by assets:
2028        view["analytics"]["distrByAssets"] = {
2029            "Ruble": {
2030                "uniques": 1,
2031                "cost": view["stat"]["availableRUB"],
2032                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Currencies": {
2035                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2036                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2037                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Shares": {
2040                "uniques": len(view["stat"]["Shares"]),
2041                "cost": view["stat"]["sharesCostRUB"],
2042                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Bonds": {
2045                "uniques": len(view["stat"]["Bonds"]),
2046                "cost": view["stat"]["bondsCostRUB"],
2047                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Etfs": {
2050                "uniques": len(view["stat"]["Etfs"]),
2051                "cost": view["stat"]["etfsCostRUB"],
2052                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Futures": {
2055                "uniques": len(view["stat"]["Futures"]),
2056                "cost": view["stat"]["futuresCostRUB"],
2057                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059        }
2060
2061        # portfolio distribution by companies:
2062        view["analytics"]["distrByCompanies"]["All money cash"] = {
2063            "ticker": "",
2064            "cost": view["stat"]["allCurrenciesCostRUB"],
2065            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2066        }
2067        view["analytics"]["distrByCompanies"].update(byComp)
2068
2069        # portfolio distribution by sectors:
2070        view["analytics"]["distrBySectors"]["All money cash"] = {
2071            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2072            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2073        }
2074        view["analytics"]["distrBySectors"].update(bySect)
2075
2076        # portfolio distribution by currencies:
2077        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2078            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2079
2080            if self.moreDebug:
2081                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2082
2083        view["analytics"]["distrByCurrencies"].update(byCurr)
2084        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2085        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2086
2087        # portfolio distribution by countries:
2088        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2089            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2090
2091            if self.moreDebug:
2092                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2093
2094        view["analytics"]["distrByCountries"].update(byCountry)
2095        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2096        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2097
2098        # --- Prepare text statistics overview in human-readable:
2099        if show:
2100            # Whatever the value `details`, header not changes:
2101            info = [
2102                "# Client's portfolio\n\n",
2103                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2104                "* **Account ID:** [{}]\n".format(self.accountId),
2105            ]
2106
2107            if details in ["full", "positions", "digest"]:
2108                info.extend([
2109                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2110                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2111                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2112                        view["stat"]["totalChangesRUB"],
2113                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2114                        view["stat"]["totalChangesPercentRUB"],
2115                    ),
2116                ])
2117
2118            if details in ["full", "positions"]:
2119                info.extend([
2120                    "## Open positions\n\n",
2121                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2122                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2123                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2124                        "{:.2f} ({:.2f}) rub".format(
2125                            view["stat"]["availableRUB"],
2126                            view["stat"]["blockedRUB"],
2127                        )
2128                    )
2129                ])
2130
2131                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2132                    return [
2133                        "|                             |                                 |          |              |              |                     |                              |\n",
2134                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2135                            noTradeStr if noTradeStr else typeStr,
2136                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2137                        ),
2138                    ]
2139
2140                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2141                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2142                        "{} [{}]".format(data["ticker"], data["figi"]),
2143                        "{:.2f} ({:.2f}) {}".format(
2144                            data["volume"],
2145                            data["blocked"],
2146                            data["currency"],
2147                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2148                            data["volume"],
2149                            data["blocked"],
2150                        ),
2151                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2152                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2153                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2154                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2155                        "{}{:.2f} {} ({}{:.2f}%)".format(
2156                            "+" if data["profit"] > 0 else "",
2157                            data["profit"], data["baseCurrencyName"],
2158                            "+" if data["percentProfit"] > 0 else "",
2159                            data["percentProfit"],
2160                        ),
2161                    )
2162
2163                # --- Show currencies section:
2164                if view["stat"]["Currencies"]:
2165                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2166                    for item in view["stat"]["Currencies"]:
2167                        info.append(_InfoStr(item, showCurrencyName=True))
2168
2169                else:
2170                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2171
2172                # --- Show shares section:
2173                if view["stat"]["Shares"]:
2174                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2175
2176                    for item in view["stat"]["Shares"]:
2177                        info.append(_InfoStr(item))
2178
2179                else:
2180                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2181
2182                # --- Show bonds section:
2183                if view["stat"]["Bonds"]:
2184                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2185
2186                    for item in view["stat"]["Bonds"]:
2187                        info.append(_InfoStr(item))
2188
2189                else:
2190                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2191
2192                # --- Show etfs section:
2193                if view["stat"]["Etfs"]:
2194                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2195
2196                    for item in view["stat"]["Etfs"]:
2197                        info.append(_InfoStr(item))
2198
2199                else:
2200                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2201
2202                # --- Show futures section:
2203                if view["stat"]["Futures"]:
2204                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2205
2206                    for item in view["stat"]["Futures"]:
2207                        info.append(_InfoStr(item))
2208
2209                else:
2210                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2211
2212            if details in ["full", "orders"]:
2213                # --- Show pending orders section:
2214                if view["stat"]["orders"]:
2215                    info.extend([
2216                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2217                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2218                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2219                    ])
2220
2221                    for item in view["stat"]["orders"]:
2222                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2223                            "{} [{}]".format(item["ticker"], item["figi"]),
2224                            item["orderID"],
2225                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2226                            "{} {} ({}{:.2f}%)".format(
2227                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2228                                item["baseCurrencyName"],
2229                                "+" if item["percentChanges"] > 0 else "",
2230                                float(item["percentChanges"]),
2231                            ),
2232                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2233                            item["action"],
2234                            item["type"],
2235                            item["date"],
2236                        ))
2237
2238                else:
2239                    info.append("\n## Total pending limit-orders: 0\n")
2240
2241                # --- Show stop orders section:
2242                if view["stat"]["stopOrders"]:
2243                    info.extend([
2244                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2245                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2246                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2247                    ])
2248
2249                    for item in view["stat"]["stopOrders"]:
2250                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2251                            "{} [{}]".format(item["ticker"], item["figi"]),
2252                            item["orderID"],
2253                            item["lotsRequested"],
2254                            "{} {} ({}{:.2f}%)".format(
2255                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2256                                item["baseCurrencyName"],
2257                                "+" if item["percentChanges"] > 0 else "",
2258                                float(item["percentChanges"]),
2259                            ),
2260                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2261                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2262                            item["action"],
2263                            item["type"],
2264                            item["expType"],
2265                            item["createDate"],
2266                            item["expDate"],
2267                        ))
2268
2269                else:
2270                    info.append("\n## Total stop-orders: 0\n")
2271
2272            if details in ["full", "analytics"]:
2273                # -- Show analytics section:
2274                if view["stat"]["portfolioCostRUB"] > 0:
2275                    info.extend([
2276                        "\n# Analytics\n"
2277                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2278                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2279                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2280                            view["stat"]["totalChangesRUB"],
2281                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2282                            view["stat"]["totalChangesPercentRUB"],
2283                        ),
2284                        "\n## Portfolio distribution by assets\n"
2285                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2286                        "|------------------------------------|---------|---------|--------------------|\n",
2287                    ])
2288
2289                    for key in view["analytics"]["distrByAssets"].keys():
2290                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2291                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2292                                key,
2293                                view["analytics"]["distrByAssets"][key]["uniques"],
2294                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2295                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2296                            ))
2297
2298                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2299
2300                    info.extend([
2301                        "\n## Portfolio distribution by companies\n"
2302                        "\n| Company                                      | Percent | Current cost       |\n",
2303                        aSepLine,
2304                    ])
2305
2306                    for company in view["analytics"]["distrByCompanies"].keys():
2307                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2308                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2309                                "{}{}".format(
2310                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2311                                    company,
2312                                ),
2313                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2314                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2315                            ))
2316
2317                    info.extend([
2318                        "\n## Portfolio distribution by sectors\n"
2319                        "\n| Sector                                       | Percent | Current cost       |\n",
2320                        aSepLine,
2321                    ])
2322
2323                    for sector in view["analytics"]["distrBySectors"].keys():
2324                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2325                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2326                                sector,
2327                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2328                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2329                            ))
2330
2331                    info.extend([
2332                        "\n## Portfolio distribution by currencies\n"
2333                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2334                        aSepLine,
2335                    ])
2336
2337                    for curr in view["analytics"]["distrByCurrencies"].keys():
2338                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2339                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2340                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2341                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2342                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2343                            ))
2344
2345                    info.extend([
2346                        "\n## Portfolio distribution by countries\n"
2347                        "\n| Assets by country                            | Percent | Current cost       |\n",
2348                        aSepLine,
2349                    ])
2350
2351                    for country in view["analytics"]["distrByCountries"].keys():
2352                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2353                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2354                                country,
2355                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2356                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2357                            ))
2358
2359            if details in ["full", "calendar"]:
2360                # -- Show bonds payment calendar section:
2361                if view["stat"]["Bonds"]:
2362                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2363                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2364                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2365
2366                else:
2367                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2368
2369            infoText = "".join(info)
2370
2371            uLogger.info(infoText)
2372
2373            if details == "full" and self.overviewFile:
2374                filename = self.overviewFile
2375
2376            elif details == "digest" and self.overviewDigestFile:
2377                filename = self.overviewDigestFile
2378
2379            elif details == "positions" and self.overviewPositionsFile:
2380                filename = self.overviewPositionsFile
2381
2382            elif details == "orders" and self.overviewOrdersFile:
2383                filename = self.overviewOrdersFile
2384
2385            elif details == "analytics" and self.overviewAnalyticsFile:
2386                filename = self.overviewAnalyticsFile
2387
2388            elif details == "calendar" and self.overviewBondsCalendarFile:
2389                filename = self.overviewBondsCalendarFile
2390
2391            else:
2392                filename = ""
2393
2394            if filename:
2395                with open(filename, "w", encoding="UTF-8") as fH:
2396                    fH.write(infoText)
2397
2398                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2399
2400        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2402    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2403        """
2404        Returns history operations between two given dates for current `accountId`.
2405        If `reportFile` string is not empty then also save human-readable report.
2406        Shows some statistical data of closed positions.
2407
2408        :param start: see docstring in `GetDatesAsString()` method
2409        :param end: see docstring in `GetDatesAsString()` method
2410        :param show: if `True` then also prints all records to the console.
2411        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2412        :return: original list of dictionaries with history of deals records from API ("operations" key):
2413                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2414                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2415        """
2416        if self.accountId is None or not self.accountId:
2417            uLogger.error("Variable `accountId` must be defined for using this method!")
2418            raise Exception("Account ID required")
2419
2420        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2421
2422        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2423
2424        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2425        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2426        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2427        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2428        customStat = {}  # custom statistics in additional to responseJSON
2429
2430        # --- output report in human-readable format:
2431        if show or self.reportFile:
2432            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2433            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2434            nextDay = ""
2435
2436            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2437
2438            if len(ops) > 0:
2439                customStat = {
2440                    "opsCount": 0,  # total operations count
2441                    "buyCount": 0,  # buy operations
2442                    "sellCount": 0,  # sell operations
2443                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2444                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2445                    "payIn": {"rub": 0.},  # Deposit brokerage account
2446                    "payOut": {"rub": 0.},  # Withdrawals
2447                    "divs": {"rub": 0.},  # Dividends income
2448                    "coupons": {"rub": 0.},  # Coupon's income
2449                    "brokerCom": {"rub": 0.},  # Service commissions
2450                    "serviceCom": {"rub": 0.},  # Service commissions
2451                    "marginCom": {"rub": 0.},  # Margin commissions
2452                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2453                }
2454
2455                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2456                for item in ops:
2457                    if item["state"] == "OPERATION_STATE_EXECUTED":
2458                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2459
2460                        # count buy operations:
2461                        if "_BUY" in item["operationType"]:
2462                            customStat["buyCount"] += 1
2463
2464                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2465                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2469
2470                        # count sell operations:
2471                        elif "_SELL" in item["operationType"]:
2472                            customStat["sellCount"] += 1
2473
2474                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2475                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2476
2477                            else:
2478                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2479
2480                        # count incoming operations:
2481                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2482                            if item["payment"]["currency"] in customStat["payIn"].keys():
2483                                customStat["payIn"][item["payment"]["currency"]] += payment
2484
2485                            else:
2486                                customStat["payIn"][item["payment"]["currency"]] = payment
2487
2488                        # count withdrawals operations:
2489                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2490                            if item["payment"]["currency"] in customStat["payOut"].keys():
2491                                customStat["payOut"][item["payment"]["currency"]] += payment
2492
2493                            else:
2494                                customStat["payOut"][item["payment"]["currency"]] = payment
2495
2496                        # count dividends income:
2497                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2498                            if item["payment"]["currency"] in customStat["divs"].keys():
2499                                customStat["divs"][item["payment"]["currency"]] += payment
2500
2501                            else:
2502                                customStat["divs"][item["payment"]["currency"]] = payment
2503
2504                        # count coupon's income:
2505                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2506                            if item["payment"]["currency"] in customStat["coupons"].keys():
2507                                customStat["coupons"][item["payment"]["currency"]] += payment
2508
2509                            else:
2510                                customStat["coupons"][item["payment"]["currency"]] = payment
2511
2512                        # count broker commissions:
2513                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2514                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2515                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2519
2520                        # count service commissions:
2521                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2522                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2523                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2527
2528                        # count margin commissions:
2529                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2530                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2531                                customStat["marginCom"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["marginCom"][item["payment"]["currency"]] = payment
2535
2536                        # count withholding taxes:
2537                        elif "_TAX" in item["operationType"]:
2538                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2539                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2543
2544                        else:
2545                            continue
2546
2547                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2548
2549                # --- view "Actions" lines:
2550                info.extend([
2551                    "| Report sections            |                               |                              |                      |                        |\n",
2552                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2553                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2554                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2555                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2556                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2557                    ),
2558                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2559                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2560                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2561                    ),
2562                ])
2563
2564                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2565                for key in opsKeys:
2566                    if key == "rub":
2567                        continue
2568
2569                    info.extend([
2570                        "|                            |                               | {:<28} |                      |                        |\n".format(
2571                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2572                        ),
2573                        "|                            |                               | {:<28} |                      |                        |\n".format(
2574                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2575                        ),
2576                    ])
2577
2578                info.append(splitLine1)
2579
2580                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2581                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2582                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2583                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2584                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2586                    )
2587
2588                # --- view "Payments" lines:
2589                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2590                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2591
2592                for key in paymentsKeys:
2593                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2594
2595                info.append(splitLine1)
2596
2597                # --- view "Commissions and taxes" lines:
2598                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2599                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2600
2601                for key in comKeys:
2602                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2603
2604                info.append(splitLine1)
2605
2606                info.extend([
2607                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2608                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2609                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2610                ])
2611
2612            else:
2613                info.append("Broker returned no operations during this period\n")
2614
2615            # --- view "Operations" section:
2616            for item in ops:
2617                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2618                    continue
2619
2620                else:
2621                    self.figi = item["figi"] if item["figi"] else ""
2622                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2623                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2624
2625                    # group of deals during one day:
2626                    if nextDay and item["date"].split("T")[0] != nextDay:
2627                        info.append(splitLine2)
2628                        nextDay = ""
2629
2630                    else:
2631                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2632
2633                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2634                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2635                        self.figi if self.figi else "—",
2636                        instrument["ticker"] if instrument else "—",
2637                        instrument["type"] if instrument else "—",
2638                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2639                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2640                        TKS_OPERATION_STATES[item["state"]],
2641                        TKS_OPERATION_TYPES[item["operationType"]],
2642                    ))
2643
2644            infoText = "".join(info)
2645
2646            if show:
2647                if self.moreDebug:
2648                    uLogger.debug("Records about history of a client's operations successfully received")
2649
2650                uLogger.info(infoText)
2651
2652            if self.reportFile:
2653                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2654                    fH.write(infoText)
2655
2656                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2657
2658        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2660    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2661        """
2662        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2663
2664        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2665        Warning! Broker server used ISO UTC time by default.
2666
2667        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2668        Also, `historyFile` used to update history with `onlyMissing` parameter.
2669
2670        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2671
2672        :param start: see docstring in `GetDatesAsString()` method.
2673        :param end: see docstring in `GetDatesAsString()` method.
2674        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2675                         `"hour"`, `"day"`. Default: `"hour"`.
2676        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2677                            False by default. Warning! History appends only from last candle to current time
2678                            with always update last candle!
2679        :param csvSep: separator if csv-file is used, `,` by default.
2680        :param show: if `True` then also prints Pandas DataFrame to the console.
2681        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2682                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2683        """
2684        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2685        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2686        history = None  # empty pandas object for history
2687
2688        if interval not in TKS_CANDLE_INTERVALS.keys():
2689            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2690            raise Exception("Incorrect value")
2691
2692        if not (self.ticker or self.figi):
2693            uLogger.error("Ticker or FIGI must be defined!")
2694            raise Exception("Ticker or FIGI required")
2695
2696        if self.ticker and not self.figi:
2697            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2698            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2699
2700        if self.figi and not self.ticker:
2701            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2702            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2703
2704        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2705        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2706        if interval.lower() != "day":
2707            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2708
2709        delta = dtEnd - dtStart  # current UTC time minus last time in file
2710        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2711
2712        # calculate history length in candles:
2713        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2714        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2715            length += 1  # to avoid fraction time
2716
2717        # calculate data blocks count:
2718        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2719
2720        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2721        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2722        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2723        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2724        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2725
2726        tempOld = None  # pandas object for old history, if --only-missing key present
2727        lastTime = None  # datetime object of last old candle in file
2728
2729        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2730            uLogger.debug("--only-missing key present, add only last missing candles...")
2731            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2732
2733            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2734
2735            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2736            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2737            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2738            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2739
2740            # get last datetime object from last string in file or minus 1 delta if file is empty:
2741            if len(tempOld) > 0:
2742                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2743
2744            else:
2745                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2746
2747            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2748
2749        responseJSONs = []  # raw history blocks of data
2750
2751        blockEnd = dtEnd
2752        for item in range(blocks):
2753            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2754            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2755
2756            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2757                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2758            ))
2759
2760            if blockStart == blockEnd:
2761                uLogger.debug("Skipped this zero-length block...")
2762
2763            else:
2764                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2765                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2766                self.body = str({
2767                    "figi": self.figi,
2768                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2769                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2770                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2771                })
2772                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2773
2774                if "code" in responseJSON.keys():
2775                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2776
2777                else:
2778                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2779                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2780
2781                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2782
2783            blockEnd = blockStart
2784
2785        printCount = len(responseJSONs)  # candles to show in console
2786        if responseJSONs:
2787            tempHistory = pd.DataFrame(
2788                data={
2789                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2790                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2791                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2792                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2793                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2794                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2795                    "volume": [int(item["volume"]) for item in responseJSONs],
2796                },
2797                index=range(len(responseJSONs)),
2798                columns=["date", "time", "open", "high", "low", "close", "volume"],
2799            )
2800            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2801            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2802
2803            # append only newest candles to old history if --only-missing key present:
2804            if onlyMissing and tempOld is not None and lastTime is not None:
2805                index = 0  # find start index in tempHistory data:
2806
2807                for i, item in tempHistory.iterrows():
2808                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2809
2810                    if curTime == lastTime:
2811                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2812                        index = i
2813                        printCount = index + 1
2814                        break
2815
2816                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2817
2818            else:
2819                history = tempHistory  # if no `--only-missing` key then load full data from server
2820
2821            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2822
2823        if history is not None and not history.empty:
2824            if show:
2825                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2826                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2827                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2828                ))
2829
2830        else:
2831            uLogger.warning("Received an empty candles history!")
2832
2833        if self.historyFile is not None:
2834            if history is not None and not history.empty:
2835                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2836                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2837
2838            else:
2839                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2840
2841        else:
2842            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2843
2844        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2846    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2847        """
2848        Load candles history from csv-file and return Pandas DataFrame object.
2849
2850        See also: `History()` and `ShowHistoryChart()` methods.
2851
2852        :param filePath: path to csv-file to open.
2853        """
2854        loadedHistory = None  # init candles data object
2855
2856        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2857
2858        if os.path.exists(filePath):
2859            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2860
2861            tfStr = self.priceModel.FormattedDelta(
2862                self.priceModel.timeframe,
2863                "{days} days {hours}h {minutes}m {seconds}s",
2864            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2865                self.priceModel.timeframe,
2866                "{hours}h {minutes}m {seconds}s",
2867            )
2868
2869            if loadedHistory is not None and not loadedHistory.empty:
2870                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2871                    len(loadedHistory),
2872                    tfStr,
2873                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2874                )
2875
2876            else:
2877                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2878
2879        else:
2880            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2881
2882        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2884    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2885        """
2886        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2887
2888        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2889        Default: `index.html` (both for interact and non-interact candlesticks chart).
2890
2891        See also: `History()` and `LoadHistory()` methods.
2892
2893        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2894        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2895                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2896                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2897                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2898        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2899                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2900        """
2901        if isinstance(candles, str):
2902            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2903            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2904
2905        elif isinstance(candles, pd.DataFrame):
2906            self.priceModel.prices = candles  # set candles chain from variable
2907            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2908
2909            if "datetime" not in candles.columns:
2910                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2911
2912        else:
2913            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2914            raise Exception("Incorrect value")
2915
2916        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2917
2918        if interact:
2919            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2920
2921            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2922
2923        else:
2924            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2925
2926            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2927
2928        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2930    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2931        """
2932        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2933        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2934
2935        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2936
2937        :param operation: string "Buy" or "Sell".
2938        :param lots: volume, integer count of lots >= 1.
2939        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2940        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2941        :param expDate: string "Undefined" by default or local date in future,
2942                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2943        :return: JSON with response from broker server.
2944        """
2945        if self.accountId is None or not self.accountId:
2946            uLogger.error("Variable `accountId` must be defined for using this method!")
2947            raise Exception("Account ID required")
2948
2949        if operation is None or not operation or operation not in ("Buy", "Sell"):
2950            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2951            raise Exception("Incorrect value")
2952
2953        if lots is None or lots < 1:
2954            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2955            lots = 1
2956
2957        if tp is None or tp < 0:
2958            tp = 0
2959
2960        if sl is None or sl < 0:
2961            sl = 0
2962
2963        if expDate is None or not expDate:
2964            expDate = "Undefined"
2965
2966        if not (self.ticker or self.figi):
2967            uLogger.error("Ticker or FIGI must be defined!")
2968            raise Exception("Ticker or FIGI required")
2969
2970        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2971        self.ticker = instrument["ticker"]
2972        self.figi = instrument["figi"]
2973
2974        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2975
2976        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2977        self.body = str({
2978            "figi": self.figi,
2979            "quantity": str(lots),
2980            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2981            "accountId": str(self.accountId),
2982            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2983        })
2984        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2985
2986        if "orderId" in response.keys():
2987            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2988                operation, response["orderId"],
2989                self.ticker, self.figi, lots,
2990                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2991                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2992                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2993            ))
2994
2995            if tp > 0:
2996                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2997
2998            if sl > 0:
2999                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3000
3001        else:
3002            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3003
3004        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3006    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3007        """
3008        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3009        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3010
3011        See also: `Order()` and `Trade()` docstrings.
3012
3013        :param lots: volume, integer count of lots >= 1.
3014        :param tp: float > 0, take profit price of stop-order.
3015        :param sl: float > 0, stop loss price of stop-order.
3016        :param expDate: it's a local date in future.
3017                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3018        :return: JSON with response from broker server.
3019        """
3020        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3022    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3023        """
3024        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3025        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3026
3027        See also: `Order()` and `Trade()` docstrings.
3028
3029        :param lots: volume, integer count of lots >= 1.
3030        :param tp: float > 0, take profit price of stop-order.
3031        :param sl: float > 0, stop loss price of stop-order.
3032        :param expDate: it's a local date in the future.
3033                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3034        :return: JSON with response from broker server.
3035        """
3036        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3038    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3039        """
3040        Close position of given instruments.
3041
3042        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3043        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3044                         This avoids unnecessary downloading data from the server.
3045        """
3046        if instruments is None or not instruments:
3047            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3048            raise Exception("Ticker or FIGI required")
3049
3050        if isinstance(instruments, str):
3051            instruments = [instruments]
3052
3053        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3054        if uniqueInstruments:
3055            if portfolio is None or not portfolio:
3056                portfolio = self.Overview(show=False)
3057
3058            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3059            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3060
3061            for self.figi in uniqueInstruments:
3062                if self.figi not in allOpened:
3063                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3064                    continue
3065
3066                # search open trade info about instrument by ticker:
3067                instrument = {}
3068                for iType in TKS_INSTRUMENTS:
3069                    if instrument:
3070                        break
3071
3072                    for item in portfolio["stat"][iType]:
3073                        if item["figi"] == self.figi:
3074                            instrument = item
3075                            break
3076
3077                if instrument:
3078                    self.ticker = instrument["ticker"]
3079                    self.figi = instrument["figi"]
3080
3081                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3082                        self.ticker,
3083                        self.figi,
3084                        int(instrument["volume"]),
3085                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3086                    ))
3087
3088                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3089
3090                    if tradeLots > 0:
3091                        if instrument["blocked"] > 0:
3092                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3093                                instrument["blocked"],
3094                                self.ticker,
3095                                tradeLots,
3096                            ))
3097
3098                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3099                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3100
3101                    else:
3102                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3104    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3105        """
3106        Close all positions of given instruments with defined type.
3107
3108        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3109        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3110                         This avoids unnecessary downloading data from the server.
3111        """
3112        if iType not in TKS_INSTRUMENTS:
3113            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3114
3115        else:
3116            if portfolio is None or not portfolio:
3117                portfolio = self.Overview(show=False)
3118
3119            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3120            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3121
3122            if tickers and portfolio:
3123                self.CloseTrades(tickers, portfolio)
3124
3125            else:
3126                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3128    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3129        """
3130        Universal method to create market or limit orders with all available parameters for current `accountId`.
3131        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3132
3133        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3134        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3135
3136        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3137        then broker immediately open market order as you can do simple --buy or --sell operations!
3138
3139        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3140        When current price will go up or down to target price value then broker opens a limit order.
3141        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3142
3143        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3144
3145        :param operation: string "Buy" or "Sell".
3146        :param orderType: string "Limit" or "Stop".
3147        :param lots: volume, integer count of lots >= 1.
3148        :param targetPrice: target price > 0. This is open trade price for limit order.
3149        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3150                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3151        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3152                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3153                         Stop loss order always executed by market price.
3154        :param expDate: string "Undefined" by default or local date in future.
3155                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3156                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3157                        A limit order has no expiration date, it lasts until the end of the trading day.
3158        :return: JSON with response from broker server.
3159        """
3160        if self.accountId is None or not self.accountId:
3161            uLogger.error("Variable `accountId` must be defined for using this method!")
3162            raise Exception("Account ID required")
3163
3164        if operation is None or not operation or operation not in ("Buy", "Sell"):
3165            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3166            raise Exception("Incorrect value")
3167
3168        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3169            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3170            raise Exception("Incorrect value")
3171
3172        if lots is None or lots < 1:
3173            uLogger.error("You must define trade volume > 0: integer count of lots!")
3174            raise Exception("Incorrect value")
3175
3176        if targetPrice is None or targetPrice <= 0:
3177            uLogger.error("Target price for limit-order must be greater than 0!")
3178            raise Exception("Incorrect value")
3179
3180        if limitPrice is None or limitPrice <= 0:
3181            limitPrice = targetPrice
3182
3183        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3184            stopType = "Limit"
3185
3186        if expDate is None or not expDate:
3187            expDate = "Undefined"
3188
3189        if not (self.ticker or self.figi):
3190            uLogger.error("Tocker or FIGI must be defined!")
3191            raise Exception("Ticker or FIGI required")
3192
3193        response = {}
3194        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3195        self.ticker = instrument["ticker"]
3196        self.figi = instrument["figi"]
3197
3198        if orderType == "Limit":
3199            uLogger.debug(
3200                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3201                    self.ticker, self.figi,
3202                    operation, lots, targetPrice, instrument["currency"],
3203                ))
3204
3205            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3206            self.body = str({
3207                "figi": self.figi,
3208                "quantity": str(lots),
3209                "price": FloatToNano(targetPrice),
3210                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3211                "accountId": str(self.accountId),
3212                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3213            })
3214            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3215
3216            if "orderId" in response.keys():
3217                uLogger.info(
3218                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3219                        response["orderId"],
3220                        self.ticker, self.figi,
3221                        operation, lots, targetPrice, instrument["currency"],
3222                    ))
3223
3224                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3225                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3226                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3227                            targetPrice, instrument["currency"],
3228                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3229                        ))
3230
3231                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3232                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3233                            targetPrice, instrument["currency"],
3234                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3235                        ))
3236
3237            else:
3238                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3239
3240        if orderType == "Stop":
3241            uLogger.debug(
3242                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3243                    self.ticker, self.figi,
3244                    operation, lots,
3245                    targetPrice, instrument["currency"],
3246                    limitPrice, instrument["currency"],
3247                    stopType, expDate,
3248                ))
3249
3250            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3251            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3252            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3253
3254            body = {
3255                "figi": self.figi,
3256                "quantity": str(lots),
3257                "price": FloatToNano(limitPrice),
3258                "stopPrice": FloatToNano(targetPrice),
3259                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3260                "accountId": str(self.accountId),
3261                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3262                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3263            }
3264
3265            if expDateUTC:
3266                body["expireDate"] = expDateUTC
3267
3268            self.body = str(body)
3269            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3270
3271            if "stopOrderId" in response.keys():
3272                uLogger.info(
3273                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3274                        response["stopOrderId"],
3275                        self.ticker, self.figi,
3276                        operation, lots,
3277                        targetPrice, instrument["currency"],
3278                        limitPrice, instrument["currency"],
3279                        TKS_STOP_ORDER_TYPES[stopOrderType],
3280                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3281                    ))
3282
3283                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3284                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3285                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3286                            targetPrice, instrument["currency"],
3287                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3288                        ))
3289
3290                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3291                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3292                            targetPrice, instrument["currency"],
3293                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3294                        ))
3295
3296            else:
3297                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3298
3299        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3301    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3302        """
3303        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3304        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3305        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3306        See also: `Order()` docstring.
3307
3308        :param lots: volume, integer count of lots >= 1.
3309        :param targetPrice: target price > 0. This is open trade price for limit order.
3310        :return: JSON with response from broker server.
3311        """
3312        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3314    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3315        """
3316        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3317        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3318        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3319        target price value then broker opens a limit order. See also: `Order()` docstring.
3320
3321        :param lots: volume, integer count of lots >= 1.
3322        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3323        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3324                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3325        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3326                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3327        :param expDate: string "Undefined" by default or local date in future.
3328                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3329                        This date is converting to UTC format for server.
3330        :return: JSON with response from broker server.
3331        """
3332        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3334    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3335        """
3336        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3337        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3338        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3339        See also: `Order()` docstring.
3340
3341        :param lots: volume, integer count of lots >= 1.
3342        :param targetPrice: target price > 0. This is open trade price for limit order.
3343        :return: JSON with response from broker server.
3344        """
3345        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3347    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3348        """
3349        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3350        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3351        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3352        target price value then broker opens a limit order. See also: `Order()` docstring.
3353
3354        :param lots: volume, integer count of lots >= 1.
3355        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3356        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3357                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3358        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3359                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3360        :param expDate: string "Undefined" by default or local date in future.
3361                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3362                        This date is converting to UTC format for server.
3363        :return: JSON with response from broker server.
3364        """
3365        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3367    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3368        """
3369        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3370
3371        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3372        :param allOrdersIDs: pre-received lists of all active pending orders.
3373                             This avoids unnecessary downloading data from the server.
3374        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3375        """
3376        if self.accountId is None or not self.accountId:
3377            uLogger.error("Variable `accountId` must be defined for using this method!")
3378            raise Exception("Account ID required")
3379
3380        if orderIDs:
3381            if allOrdersIDs is None or not allOrdersIDs:
3382                rawOrders = self.RequestPendingOrders()
3383                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3384
3385            if allStopOrdersIDs is None or not allStopOrdersIDs:
3386                rawStopOrders = self.RequestStopOrders()
3387                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3388
3389            for orderID in orderIDs:
3390                idInPendingOrders = orderID in allOrdersIDs
3391                idInStopOrders = orderID in allStopOrdersIDs
3392
3393                if not (idInPendingOrders or idInStopOrders):
3394                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3395                    continue
3396
3397                else:
3398                    if idInPendingOrders:
3399                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3400
3401                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3402                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3403                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3404                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3405
3406                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3407                            if self.moreDebug:
3408                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3409
3410                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3411
3412                        else:
3413                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3414
3415                    elif idInStopOrders:
3416                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3417
3418                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3419                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3420                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3421                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3422
3423                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3424                            if self.moreDebug:
3425                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3426
3427                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3428
3429                        else:
3430                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3431
3432                    else:
3433                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3435    def CloseAllOrders(self) -> None:
3436        """
3437        Gets a list of open pending and stop orders and cancel it all.
3438        """
3439        rawOrders = self.RequestPendingOrders()
3440        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3441        lenOrders = len(allOrdersIDs)
3442
3443        rawStopOrders = self.RequestStopOrders()
3444        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3445        lenSOrders = len(allStopOrdersIDs)
3446
3447        if lenOrders > 0 or lenSOrders > 0:
3448            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3449
3450            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3451
3452        else:
3453            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3455    def CloseAll(self, *args) -> None:
3456        """
3457        Close all available (not blocked) opened trades and orders.
3458
3459        Also, you can select one or more keywords case-insensitive:
3460        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3461
3462        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3463        """
3464        overview = self.Overview(show=False)  # get all open trades info
3465
3466        if len(args) == 0:
3467            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3468            self.CloseAllOrders()  # close all pending and stop orders
3469
3470            for iType in TKS_INSTRUMENTS:
3471                if iType != "Currencies":
3472                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3473
3474        else:
3475            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3476            lowerArgs = [x.lower() for x in args]
3477
3478            if "orders" in lowerArgs:
3479                self.CloseAllOrders()  # close all pending and stop orders
3480
3481            for iType in TKS_INSTRUMENTS:
3482                if iType.lower() in lowerArgs and iType != "Currencies":
3483                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3485    @staticmethod
3486    def ParseOrderParameters(operation, **inputParameters):
3487        """
3488        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3489
3490        :param operation: string "Buy" or "Sell".
3491        :param inputParameters: this is dict of strings that looks like this
3492               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3493               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3494               "prices" key: one or more prices to open limit-orders
3495               Counts of values in lots and prices lists must be equals!
3496        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3497        """
3498        # TODO: update order grid work with api v2
3499        pass
3500        # uLogger.debug("Input parameters: {}".format(inputParameters))
3501        #
3502        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3503        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3504        #     raise Exception("Incorrect value")
3505        #
3506        # if "l" in inputParameters.keys():
3507        #     inputParameters["lots"] = inputParameters.pop("l")
3508        #
3509        # if "p" in inputParameters.keys():
3510        #     inputParameters["prices"] = inputParameters.pop("p")
3511        #
3512        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3513        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3514        #     raise Exception("Incorrect value")
3515        #
3516        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3517        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3518        #
3519        # if len(lots) != len(prices):
3520        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3521        #     raise Exception("Incorrect value")
3522        #
3523        # uLogger.debug("Extracted parameters for orders:")
3524        # uLogger.debug("lots = {}".format(lots))
3525        # uLogger.debug("prices = {}".format(prices))
3526        #
3527        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3528        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3529        # uLogger.debug("Order parameters: {}".format(result))
3530        #
3531        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3533    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3534        """
3535        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3536
3537        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3538        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3539        """
3540        result = False
3541        msg = "Instrument not defined!"
3542
3543        if portfolio is None or not portfolio:
3544            portfolio = self.Overview(show=False)
3545
3546        if self.ticker:
3547            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3548            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3549
3550            for iType in TKS_INSTRUMENTS:
3551                for instrument in portfolio["stat"][iType]:
3552                    if instrument["ticker"] == self.ticker:
3553                        result = True
3554                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3555                        break
3556
3557        elif self.figi:
3558            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3559            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3560
3561            for iType in TKS_INSTRUMENTS:
3562                for instrument in portfolio["stat"][iType]:
3563                    if instrument["figi"] == self.figi:
3564                        result = True
3565                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3566                        break
3567
3568        else:
3569            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3570
3571        uLogger.debug(msg)
3572
3573        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3575    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3576        """
3577        Returns instrument from the user's portfolio if it presents there.
3578        Instrument must be defined by `ticker` (highly priority) or `figi`.
3579
3580        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3581        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3582        """
3583        result = None
3584        msg = "Instrument not defined!"
3585
3586        if portfolio is None or not portfolio:
3587            portfolio = self.Overview(show=False)
3588
3589        if self.ticker:
3590            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3591            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3592
3593            for iType in TKS_INSTRUMENTS:
3594                for instrument in portfolio["stat"][iType]:
3595                    if instrument["ticker"] == self.ticker:
3596                        result = instrument
3597                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3598                        break
3599
3600        elif self.figi:
3601            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3602            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3603
3604            for iType in TKS_INSTRUMENTS:
3605                for instrument in portfolio["stat"][iType]:
3606                    if instrument["figi"] == self.figi:
3607                        result = instrument
3608                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3609                        break
3610
3611        else:
3612            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3613
3614        uLogger.debug(msg)
3615
3616        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3618    def RequestLimits(self) -> dict:
3619        """
3620        Method for obtaining the available funds for withdrawal for current `accountId`.
3621
3622        See also:
3623        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3624        - `OverviewLimits()` method
3625
3626        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3627                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3628                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3629                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3630        """
3631        if self.accountId is None or not self.accountId:
3632            uLogger.error("Variable `accountId` must be defined for using this method!")
3633            raise Exception("Account ID required")
3634
3635        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3636
3637        self.body = str({"accountId": self.accountId})
3638        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3639        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3640
3641        if self.moreDebug:
3642            uLogger.debug("Records about available funds for withdrawal successfully received")
3643
3644        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3646    def OverviewLimits(self, show: bool = False) -> dict:
3647        """
3648        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3649
3650        See also: `RequestLimits()`.
3651
3652        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3653        :return: dict with raw parsed data from server and some calculated statistics about it.
3654        """
3655        if self.accountId is None or not self.accountId:
3656            uLogger.error("Variable `accountId` must be defined for using this method!")
3657            raise Exception("Account ID required")
3658
3659        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3660
3661        view = {
3662            "rawLimits": rawLimits,
3663            "limits": {  # parsed data for every currency:
3664                "money": {  # this is an array of portfolio currency positions
3665                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3666                },
3667                "blocked": {  # this is an array of blocked currency
3668                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3669                },
3670                "blockedGuarantee": {  # this is locked money under collateral for futures
3671                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3672                },
3673            },
3674        }
3675
3676        # --- Prepare text table with limits in human-readable format:
3677        if show:
3678            info = [
3679                "# Withdrawal limits\n\n",
3680                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3681                "* **Account ID:** [{}]\n".format(self.accountId),
3682            ]
3683
3684            if view["limits"]["money"]:
3685                info.extend([
3686                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3687                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3688                ])
3689
3690            else:
3691                info.append("\nNo withdrawal limits\n")
3692
3693            for curr in view["limits"]["money"].keys():
3694                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3695                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3696                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3697
3698                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3699                    "[{}]".format(curr),
3700                    "{:.2f}".format(view["limits"]["money"][curr]),
3701                    "{:.2f}".format(availableMoney),
3702                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3703                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3704                )
3705
3706                if curr == "rub":
3707                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3708
3709                else:
3710                    info.append(infoStr)
3711
3712            infoText = "".join(info)
3713
3714            uLogger.info(infoText)
3715
3716            if self.withdrawalLimitsFile:
3717                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3718                    fH.write(infoText)
3719
3720                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3721
3722        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3724    def RequestAccounts(self) -> dict:
3725        """
3726        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3727
3728        See also:
3729        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3730        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3731        - `OverviewUserInfo()` method
3732
3733        :return: dict with raw data from server that contains accounts info. Example of dict:
3734                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3735                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3736                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3737                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3738        """
3739        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3740
3741        self.body = str({})
3742        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3743        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3744
3745        if self.moreDebug:
3746            uLogger.debug("Records about available accounts successfully received")
3747
3748        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3750    def RequestUserInfo(self) -> dict:
3751        """
3752        Method for requesting common user's information.
3753
3754        See also:
3755        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3756        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3757        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3758        - `OverviewUserInfo()` method
3759
3760        :return: dict with raw data from server that contains user's information. Example of dict:
3761                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3762                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3763        """
3764        uLogger.debug("Requesting common user's information. Wait, please...")
3765
3766        self.body = str({})
3767        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3768        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3769
3770        if self.moreDebug:
3771            uLogger.debug("Records about current user successfully received")
3772
3773        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3775    def RequestMarginStatus(self, accountId: str = None) -> dict:
3776        """
3777        Method for requesting margin calculation for defined account ID.
3778
3779        See also:
3780        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3781        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3782        - `OverviewUserInfo()` method
3783
3784        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3785        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3786                 Example of responses:
3787                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3788                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3789                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3790                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3791                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3792                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3793        """
3794        if accountId is None or not accountId:
3795            if self.accountId is None or not self.accountId:
3796                uLogger.error("Variable `accountId` must be defined for using this method!")
3797                raise Exception("Account ID required")
3798
3799            else:
3800                accountId = self.accountId  # use `self.accountId` (main ID) by default
3801
3802        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3803
3804        self.body = str({"accountId": accountId})
3805        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3806        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3807
3808        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3809            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3810            rawMargin = {}
3811
3812        else:
3813            if self.moreDebug:
3814                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3815
3816        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3818    def RequestTariffLimits(self) -> dict:
3819        """
3820        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3821
3822        See also:
3823        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3824        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3825        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3826        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3827        - `OverviewUserInfo()` method
3828
3829        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3830                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3831                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3832        """
3833        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3834
3835        self.body = str({})
3836        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3837        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3838
3839        if self.moreDebug:
3840            uLogger.debug("Records with limits of current tariff successfully received")
3841
3842        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3844    def RequestBondCoupons(self, iJSON: dict) -> dict:
3845        """
3846        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3847        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3848        All dates are in UTC timezone.
3849
3850        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3851        Documentation:
3852        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3853        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3854
3855        See also: `ExtendBondsData()`.
3856
3857        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3858                      If raw iJSON is not data of bond then server returns an error [400] with message:
3859                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3860        :return: dictionary with bond payment calendar. Response example
3861                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3862                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3863                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3864                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3865        """
3866        if iJSON["figi"] is None or not iJSON["figi"]:
3867            uLogger.error("FIGI must be defined for using this method!")
3868            raise Exception("FIGI required")
3869
3870        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3871        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3872
3873        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3874            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3875            self.figi,
3876            startDate,
3877            endDate,
3878        ))
3879
3880        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3881        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3882        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3883
3884        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3885            uLogger.warning("Instrument type is not bond!")
3886
3887        else:
3888            if self.moreDebug:
3889                uLogger.debug("Records about bond payment calendar successfully received")
3890
3891        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3893    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3894        """
3895        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3896        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3897        coupon yields, current yields and some statistics etc.
3898
3899        WARNING! This is too long operation if a lot of bonds requested from broker server.
3900
3901        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3902
3903        :param instruments: list of strings with tickers or FIGIs.
3904        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3905                     for further used by data scientists or stock analytics.
3906        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3907                 In XLSX-file and Pandas DataFrame fields mean:
3908                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3909                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3910        """
3911        if instruments is None or not instruments:
3912            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3913            raise Exception("Ticker or FIGI required")
3914
3915        if isinstance(instruments, str):
3916            instruments = [instruments]
3917
3918        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3919
3920        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3921
3922        iCount = len(uniqueInstruments)
3923        tooLong = iCount >= 20
3924        if tooLong:
3925            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3926
3927        bonds = None
3928        for i, self.figi in enumerate(uniqueInstruments):
3929            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3930
3931            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3932                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3933                rawBond = self.SearchByFIGI(requestPrice=True)
3934
3935                # Widen raw data with UTC current time (iData["actualDateTime"]):
3936                actualDate = datetime.now(tzutc())
3937                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3938
3939                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3940                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3941
3942                # Replace some values with human-readable:
3943                iData["nominalCurrency"] = iData["nominal"]["currency"]
3944                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3945                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3946                iData["aciCurrency"] = iData["aciValue"]["currency"]
3947                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3948                iData["issueSize"] = int(iData["issueSize"])
3949                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3950                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3951                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3952                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3953                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3954                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3955                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3956                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3957                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3958                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3959
3960                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3961                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3962                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3963                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3964                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3965                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3966                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3967                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3968                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3969                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3970                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3971
3972                # Widen raw data with calendar data from `rawCalendar` values:
3973                calendarData = []
3974                if "events" in iData["rawCalendar"].keys():
3975                    for item in iData["rawCalendar"]["events"]:
3976                        calendarData.append({
3977                            "couponDate": item["couponDate"],
3978                            "couponNumber": int(item["couponNumber"]),
3979                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3980                            "payCurrency": item["payOneBond"]["currency"],
3981                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3982                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3983                            "couponStartDate": item["couponStartDate"],
3984                            "couponEndDate": item["couponEndDate"],
3985                            "couponPeriod": item["couponPeriod"],
3986                        })
3987
3988                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3989                    if "maturityDate" not in iData.keys():
3990                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3991
3992                # Widen raw data with Coupon Rate.
3993                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3994                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3995                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3996                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3997
3998                # Widen raw data with Yield to Maturity (YTM) on current date.
3999                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4000                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4001                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4002                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4003                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4004                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4005
4006                iData["calendar"] = calendarData  # adds calendar at the end
4007
4008                # Remove not used data:
4009                iData.pop("uid")
4010                iData.pop("positionUid")
4011                iData.pop("currentPrice")
4012                iData.pop("rawCalendar")
4013
4014                colNames = list(iData.keys())
4015                if bonds is None:
4016                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4017
4018                else:
4019                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4020
4021            else:
4022                uLogger.warning("Instrument is not a bond!")
4023
4024            processed = round(100 * (i + 1) / iCount, 1)
4025            if tooLong and processed % 5 == 0:
4026                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4027
4028            else:
4029                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4030
4031        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4032
4033        # Saving bonds from Pandas DataFrame to XLSX sheet:
4034        if xlsx and self.bondsXLSXFile:
4035            with pd.ExcelWriter(
4036                    path=self.bondsXLSXFile,
4037                    date_format=TKS_DATE_FORMAT,
4038                    datetime_format=TKS_DATE_TIME_FORMAT,
4039                    mode="w",
4040            ) as writer:
4041                bonds.to_excel(
4042                    writer,
4043                    sheet_name="Extended bonds data",
4044                    index=True,
4045                    encoding="UTF-8",
4046                    freeze_panes=(1, 1),
4047                )  # saving as XLSX-file with freeze first row and column as headers
4048
4049            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4050
4051        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4053    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4054        """
4055        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4056
4057        WARNING! This is too long operation if a lot of bonds requested from broker server.
4058
4059        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4060
4061        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4062                        extended information about bonds: main info, current prices, bond payment calendar,
4063                        coupon yields, current yields and some statistics etc.
4064                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4065        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4066                     for further used by data scientists or stock analytics.
4067        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4068        """
4069        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4070            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4071
4072        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4073
4074        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4075        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4076        calendar = None
4077        for bond in extBonds.iterrows():
4078            for item in bond[1]["calendar"]:
4079                cData = {
4080                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4081                    "couponDate": item["couponDate"],
4082                    "figi": bond[1]["figi"],
4083                    "ticker": bond[1]["ticker"],
4084                    "name": bond[1]["name"],
4085                    "couponNumber": item["couponNumber"],
4086                    "payOneBond": item["payOneBond"],
4087                    "payCurrency": item["payCurrency"],
4088                    "couponType": item["couponType"],
4089                    "couponPeriod": item["couponPeriod"],
4090                    "fixDate": item["fixDate"],
4091                    "couponStartDate": item["couponStartDate"],
4092                    "couponEndDate": item["couponEndDate"],
4093                }
4094
4095                if calendar is None:
4096                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4097
4098                else:
4099                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4100
4101        if calendar is not None:
4102            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4103
4104            # Saving calendar from Pandas DataFrame to XLSX sheet:
4105            if xlsx:
4106                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4107
4108                with pd.ExcelWriter(
4109                        path=xlsxCalendarFile,
4110                        date_format=TKS_DATE_FORMAT,
4111                        datetime_format=TKS_DATE_TIME_FORMAT,
4112                        mode="w",
4113                ) as writer:
4114                    humanReadable = calendar.copy(deep=True)
4115                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4116                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4117                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4118                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4119                    humanReadable.columns = colNames  # human-readable column names
4120
4121                    humanReadable.to_excel(
4122                        writer,
4123                        sheet_name="Bond payments calendar",
4124                        index=False,
4125                        encoding="UTF-8",
4126                        freeze_panes=(1, 2),
4127                    )  # saving as XLSX-file with freeze first row and column as headers
4128
4129                    del humanReadable  # release df in memory
4130
4131                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4132
4133        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4135    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4136        """
4137        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4138        Also, creates Markdown file with calendar data, `calendar.md` by default.
4139
4140        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4141
4142        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4143                        extended information about bonds: main info, current prices, bond payment calendar,
4144                        coupon yields, current yields and some statistics etc.
4145                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4146        :param show: if `True` then also printing bonds payment calendar to the console,
4147                     otherwise save to file `calendarFile` only. `False` by default.
4148        :return: multilines text in Markdown format with bonds payment calendar as a table.
4149        """
4150        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4151            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4152
4153        infoText = "# Bond payments calendar\n\n"
4154
4155        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4156
4157        if not (calendar is None or calendar.empty):
4158            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4159
4160            info = [
4161                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4162                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4163            ]
4164
4165            newMonth = False
4166            notOneBond = calendar["figi"].nunique() > 1
4167            for i, bond in enumerate(calendar.iterrows()):
4168                if newMonth and notOneBond:
4169                    info.append(splitLine)
4170
4171                info.append(
4172                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4173                        "  √" if bond[1]["paid"] else "  —",
4174                        bond[1]["couponDate"].split("T")[0],
4175                        bond[1]["figi"],
4176                        bond[1]["ticker"],
4177                        bond[1]["couponNumber"],
4178                        "{} {}".format(
4179                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4180                            bond[1]["payCurrency"],
4181                        ),
4182                        bond[1]["couponType"],
4183                        bond[1]["couponPeriod"],
4184                        bond[1]["fixDate"].split("T")[0],
4185                    )
4186                )
4187
4188                if i < len(calendar.values) - 1:
4189                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4190                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4191                    newMonth = False if curDate.month == nextDate.month else True
4192
4193                else:
4194                    newMonth = False
4195
4196            infoText += "".join(info)
4197
4198            if show:
4199                uLogger.info("{}".format(infoText))
4200
4201            if self.calendarFile is not None:
4202                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4203                    fH.write(infoText)
4204
4205                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4206
4207        else:
4208            infoText += "No data\n"
4209
4210        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4212    def OverviewAccounts(self, show: bool = False) -> dict:
4213        """
4214        Method for parsing and show simple table with all available user accounts.
4215
4216        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4217
4218        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4219        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4220                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4221                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4222                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4223                                                        "closed": "—", "access": "Full access" }, ...}}`
4224        """
4225        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4226
4227        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4228        accounts = {
4229            item["id"]: {
4230                "type": TKS_ACCOUNT_TYPES[item["type"]],
4231                "name": item["name"],
4232                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4233                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4234                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4235                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4236            } for item in rawAccounts["accounts"]
4237        }
4238
4239        # Raw and parsed data with some fields replaced in "stat" section:
4240        view = {
4241            "rawAccounts": rawAccounts,
4242            "stat": accounts,
4243        }
4244
4245        # --- Prepare simple text table with only accounts data in human-readable format:
4246        if show:
4247            info = [
4248                "# User accounts\n\n",
4249                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4250                "| Account ID   | Type                      | Status                    | Name                           |\n",
4251                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4252            ]
4253
4254            for account in view["stat"].keys():
4255                info.extend([
4256                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4257                        account,
4258                        view["stat"][account]["type"],
4259                        view["stat"][account]["status"],
4260                        view["stat"][account]["name"],
4261                    )
4262                ])
4263
4264            infoText = "".join(info)
4265
4266            uLogger.info(infoText)
4267
4268            if self.userAccountsFile:
4269                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4270                    fH.write(infoText)
4271
4272                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4273
4274        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4276    def OverviewUserInfo(self, show: bool = False) -> dict:
4277        """
4278        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4279
4280        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4281
4282        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4283        :return: dict with raw parsed data from server and some calculated statistics about it.
4284        """
4285        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4286        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4287        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4288        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4289        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4290        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4291
4292        # This is dict with parsed common user data:
4293        userInfo = {
4294            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4295            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4296            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4297            "tariff": rawUserInfo["tariff"],
4298        }
4299
4300        # This is an array of dict with parsed margin statuses for every account IDs:
4301        margins = {}
4302        for accountId in accounts.keys():
4303            if rawMargins[accountId]:
4304                margins[accountId] = {
4305                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4306                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4307                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4308                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4309                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4310                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4311                }
4312
4313            else:
4314                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4315
4316        unary = {}  # unary-connection limits
4317        for item in rawTariffLimits["unaryLimits"]:
4318            if item["limitPerMinute"] in unary.keys():
4319                unary[item["limitPerMinute"]].extend(item["methods"])
4320
4321            else:
4322                unary[item["limitPerMinute"]] = item["methods"]
4323
4324        stream = {}  # stream-connection limits
4325        for item in rawTariffLimits["streamLimits"]:
4326            if item["limit"] in stream.keys():
4327                stream[item["limit"]].extend(item["streams"])
4328
4329            else:
4330                stream[item["limit"]] = item["streams"]
4331
4332        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4333        limits = {
4334            "unary": unary,
4335            "stream": stream,
4336        }
4337
4338        # Raw and parsed data as an output result:
4339        view = {
4340            "rawUserInfo": rawUserInfo,
4341            "rawAccounts": rawAccounts,
4342            "rawMargins": rawMargins,
4343            "rawTariffLimits": rawTariffLimits,
4344            "stat": {
4345                "userInfo": userInfo,
4346                "accounts": accounts,
4347                "margins": margins,
4348                "limits": limits,
4349            },
4350        }
4351
4352        # --- Prepare text table with user information in human-readable format:
4353        if show:
4354            info = [
4355                "# Full user information\n\n",
4356                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4357                "## Common information\n\n",
4358                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4359                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4360                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4361                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4362                "\n## User accounts\n\n",
4363            ]
4364
4365            for account in view["stat"]["accounts"].keys():
4366                info.extend([
4367                    "### ID: [{}]\n\n".format(account),
4368                    "| Parameters           | Values                                                       |\n",
4369                    "|----------------------|--------------------------------------------------------------|\n",
4370                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4371                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4372                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4373                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4374                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4375                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4376                ])
4377
4378                if margins[account]:
4379                    info.extend([
4380                        "| Margin status:       | Enabled                                                      |\n",
4381                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4382                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4383                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4384                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4385                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4386                    ])
4387
4388                else:
4389                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4390
4391            info.extend([
4392                "\n## Current user tariff limits\n",
4393                "\nSee also:\n",
4394                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4395                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4396                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4397                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4398                "\n### Unary limits\n",
4399            ])
4400
4401            if unary:
4402                for key, values in sorted(unary.items()):
4403                    info.append("\n* Max requests per minute: {}\n".format(key))
4404
4405                    for value in values:
4406                        info.append("  - {}\n".format(value))
4407
4408            else:
4409                info.append("\nNot available\n")
4410
4411            info.append("\n### Stream limits\n")
4412
4413            if stream:
4414                for key, values in sorted(stream.items()):
4415                    info.append("\n* Max stream connections: {}\n".format(key))
4416
4417                    for value in values:
4418                        info.append("  - {}\n".format(value))
4419
4420            else:
4421                info.append("\nNot available\n")
4422
4423            infoText = "".join(info)
4424
4425            uLogger.info(infoText)
4426
4427            if self.userInfoFile:
4428                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4429                    fH.write(infoText)
4430
4431                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4432
4433        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4436class Args:
4437    """
4438    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4439    """
4440    def __init__(self, **kwargs):
4441        self.__dict__.update(kwargs)
4442
4443    def __getattr__(self, item):
4444        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4440    def __init__(self, **kwargs):
4441        self.__dict__.update(kwargs)
def ParseArgs()
4447def ParseArgs():
4448    """This function get and parse command line keys."""
4449    parser = ArgumentParser()  # command-line string parser
4450
4451    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4452    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4453
4454    # --- options:
4455
4456    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4457    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4458    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4459
4460    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4461    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4462
4463    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4464    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4465
4466    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4467
4468    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4469    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4470    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4471
4472    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4473    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4474
4475    # --- commands:
4476
4477    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4478
4479    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4480    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4481    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4482    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4483    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4484    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4485    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4486    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4487
4488    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4489    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4490    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4491    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4492    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4493    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4494
4495    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4496    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4497    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4498    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4499
4500    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4501    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4502    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4503
4504    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4505    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4506    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4507    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4508    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4509    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4510    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4511
4512    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4513    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4514    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4515    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4516    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4517
4518    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4519    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4520    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4521
4522    cmdArgs = parser.parse_args()
4523    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4526def Main(**kwargs):
4527    """
4528    Main function for work with TKSBrokerAPI in the console.
4529
4530    See examples:
4531    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4532    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4533    """
4534    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4535
4536    if args.debug_level:
4537        uLogger.level = 10  # always debug level by default
4538        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4539
4540    exitCode = 0
4541    start = datetime.now(tzutc())
4542    uLogger.debug("=-" * 50)
4543    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4544        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4545        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4546    ))
4547
4548    # trying to calculate full current version:
4549    buildVersion = __version__
4550    try:
4551        v = version("tksbrokerapi")
4552        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4553
4554    except Exception:
4555        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4556
4557    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4558    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4559
4560    try:
4561        if args.version:
4562            print("TKSBrokerAPI {}".format(buildVersion))
4563            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4564
4565        else:
4566            # Init class for trading with Tinkoff Broker:
4567            trader = TinkoffBrokerServer(
4568                token=args.token,
4569                accountId=args.account_id,
4570                useCache=not args.no_cache,
4571            )
4572
4573            # --- set some options:
4574
4575            if args.more:
4576                trader.moreDebug = True
4577                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4578
4579            if args.ticker:
4580                if args.ticker in trader.aliasesKeys:
4581                    trader.ticker = trader.aliases[args.ticker]  # Replace some tickers with its aliases
4582
4583                else:
4584                    trader.ticker = args.ticker
4585
4586            if args.figi:
4587                trader.figi = args.figi
4588
4589            if args.depth is not None:
4590                trader.depth = args.depth
4591
4592            # --- do one command:
4593
4594            if args.list:
4595                if args.output is not None:
4596                    trader.instrumentsFile = args.output
4597
4598                trader.ShowInstrumentsInfo(show=True)
4599
4600            elif args.list_xlsx:
4601                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4602
4603            elif args.bonds_xlsx is not None:
4604                if args.output is not None:
4605                    trader.bondsXLSXFile = args.output
4606
4607                if len(args.bonds_xlsx) == 0:
4608                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4609
4610                else:
4611                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4612
4613            elif args.search:
4614                if args.output is not None:
4615                    trader.searchResultsFile = args.output
4616
4617                trader.SearchInstruments(pattern=args.search[0], show=True)
4618
4619            elif args.info:
4620                if not (args.ticker or args.figi):
4621                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4622                    raise Exception("Ticker or FIGI required")
4623
4624                if args.output is not None:
4625                    trader.infoFile = args.output
4626
4627                if args.ticker:
4628                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4629
4630                else:
4631                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4632
4633            elif args.calendar is not None:
4634                if args.output is not None:
4635                    trader.calendarFile = args.output
4636
4637                if len(args.calendar) == 0:
4638                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4639
4640                else:
4641                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4642
4643                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4644
4645            elif args.price:
4646                if not (args.ticker or args.figi):
4647                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4648                    raise Exception("Ticker or FIGI required")
4649
4650                trader.GetCurrentPrices(show=True)
4651
4652            elif args.prices is not None:
4653                if args.output is not None:
4654                    trader.pricesFile = args.output
4655
4656                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4657
4658            elif args.overview:
4659                if args.output is not None:
4660                    trader.overviewFile = args.output
4661
4662                trader.Overview(show=True, details="full")
4663
4664            elif args.overview_digest:
4665                if args.output is not None:
4666                    trader.overviewDigestFile = args.output
4667
4668                trader.Overview(show=True, details="digest")
4669
4670            elif args.overview_positions:
4671                if args.output is not None:
4672                    trader.overviewPositionsFile = args.output
4673
4674                trader.Overview(show=True, details="positions")
4675
4676            elif args.overview_orders:
4677                if args.output is not None:
4678                    trader.overviewOrdersFile = args.output
4679
4680                trader.Overview(show=True, details="orders")
4681
4682            elif args.overview_analytics:
4683                if args.output is not None:
4684                    trader.overviewAnalyticsFile = args.output
4685
4686                trader.Overview(show=True, details="analytics")
4687
4688            elif args.overview_calendar:
4689                if args.output is not None:
4690                    trader.overviewAnalyticsFile = args.output
4691
4692                trader.Overview(show=True, details="calendar")
4693
4694            elif args.deals is not None:
4695                if args.output is not None:
4696                    trader.reportFile = args.output
4697
4698                if 0 <= len(args.deals) < 3:
4699                    trader.Deals(
4700                        start=args.deals[0] if len(args.deals) >= 1 else None,
4701                        end=args.deals[1] if len(args.deals) == 2 else None,
4702                        show=True,  # Always show deals report in console
4703                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4704                    )
4705
4706                else:
4707                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4708                    raise Exception("Incorrect value")
4709
4710            elif args.history is not None:
4711                if args.output is not None:
4712                    trader.historyFile = args.output
4713
4714                if 0 <= len(args.history) < 3:
4715                    dataReceived = trader.History(
4716                        start=args.history[0] if len(args.history) >= 1 else None,
4717                        end=args.history[1] if len(args.history) == 2 else None,
4718                        interval="hour" if args.interval is None or not args.interval else args.interval,
4719                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4720                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4721                        show=True,  # shows all downloaded candles in console
4722                    )
4723
4724                    if args.render_chart is not None and dataReceived is not None:
4725                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4726
4727                        trader.ShowHistoryChart(
4728                            candles=dataReceived,
4729                            interact=iChart,
4730                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4731                        )
4732
4733                else:
4734                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4735                    raise Exception("Incorrect value")
4736
4737            elif args.load_history is not None:
4738                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4739
4740                if args.render_chart is not None and histData is not None:
4741                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4742                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4743
4744                    trader.ShowHistoryChart(
4745                        candles=histData,
4746                        interact=iChart,
4747                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4748                    )
4749
4750            elif args.trade is not None:
4751                if 1 <= len(args.trade) <= 5:
4752                    trader.Trade(
4753                        operation=args.trade[0],
4754                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4755                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4756                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4757                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4758                    )
4759
4760                else:
4761                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4762
4763            elif args.buy is not None:
4764                if 0 <= len(args.buy) <= 4:
4765                    trader.Buy(
4766                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4767                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4768                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4769                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4770                    )
4771
4772                else:
4773                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4774
4775            elif args.sell is not None:
4776                if 0 <= len(args.sell) <= 4:
4777                    trader.Sell(
4778                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4779                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4780                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4781                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4782                    )
4783
4784                else:
4785                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4786
4787            elif args.order:
4788                if 4 <= len(args.order) <= 7:
4789                    trader.Order(
4790                        operation=args.order[0],
4791                        orderType=args.order[1],
4792                        lots=int(args.order[2]),
4793                        targetPrice=float(args.order[3]),
4794                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4795                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4796                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4797                    )
4798
4799                else:
4800                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4801
4802            elif args.buy_limit:
4803                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4804
4805            elif args.sell_limit:
4806                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4807
4808            elif args.buy_stop:
4809                if 2 <= len(args.buy_stop) <= 7:
4810                    trader.BuyStop(
4811                        lots=int(args.buy_stop[0]),
4812                        targetPrice=float(args.buy_stop[1]),
4813                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4814                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4815                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4816                    )
4817
4818                else:
4819                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4820
4821            elif args.sell_stop:
4822                if 2 <= len(args.sell_stop) <= 7:
4823                    trader.SellStop(
4824                        lots=int(args.sell_stop[0]),
4825                        targetPrice=float(args.sell_stop[1]),
4826                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4827                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4828                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4829                    )
4830
4831                else:
4832                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4833
4834            # elif args.buy_order_grid is not None:
4835            #     # update order grid work with api v2
4836            #     if len(args.buy_order_grid) == 2:
4837            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4838            #
4839            #         for order in orderParams:
4840            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4841            #
4842            #     else:
4843            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4844            #
4845            # elif args.sell_order_grid is not None:
4846            #     # update order grid work with api v2
4847            #     if len(args.sell_order_grid) >= 2:
4848            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4849            #
4850            #         for order in orderParams:
4851            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4852            #
4853            #     else:
4854            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4855
4856            elif args.close_order is not None:
4857                trader.CloseOrders(args.close_order)  # close only one order
4858
4859            elif args.close_orders is not None:
4860                trader.CloseOrders(args.close_orders)  # close list of orders
4861
4862            elif args.close_trade:
4863                if not (args.ticker or args.figi):
4864                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4865                    raise Exception("Ticker or FIGI required")
4866
4867                if args.ticker:
4868                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4869
4870                else:
4871                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4872
4873            elif args.close_trades is not None:
4874                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4875
4876            elif args.close_all is not None:
4877                trader.CloseAll(*args.close_all)
4878
4879            elif args.limits:
4880                if args.output is not None:
4881                    trader.withdrawalLimitsFile = args.output
4882
4883                trader.OverviewLimits(show=True)
4884
4885            elif args.user_info:
4886                if args.output is not None:
4887                    trader.userInfoFile = args.output
4888
4889                trader.OverviewUserInfo(show=True)
4890
4891            elif args.account:
4892                if args.output is not None:
4893                    trader.userAccountsFile = args.output
4894
4895                trader.OverviewAccounts(show=True)
4896
4897            else:
4898                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4899                raise Exception("There is no command to execute")
4900
4901    except Exception:
4902        trace = tb.format_exc()
4903        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4904            if e in trace:
4905                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4906                break
4907
4908        uLogger.debug(trace)
4909        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4910        exitCode = 255  # an error occurred, must be open a ticket for this issue
4911
4912    finally:
4913        finish = datetime.now(tzutc())
4914
4915        if exitCode == 0:
4916            if args.more:
4917                uLogger.debug("All operations were finished success (summary code is 0).")
4918
4919        else:
4920            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4921                os.path.abspath(uLog.defaultLogFile), exitCode,
4922            ))
4923
4924        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4925        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4926            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4927            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4928        ))
4929        uLogger.debug("=-" * 50)
4930
4931        if not kwargs:
4932            sys.exit(exitCode)
4933
4934        else:
4935            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: